diff --git a/core/src/main/java/org/elasticsearch/action/ActionModule.java b/core/src/main/java/org/elasticsearch/action/ActionModule.java index 94db7d2030851..c1d0541d4ce10 100644 --- a/core/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/core/src/main/java/org/elasticsearch/action/ActionModule.java @@ -149,6 +149,9 @@ import org.elasticsearch.action.delete.TransportDeleteAction; import org.elasticsearch.action.explain.ExplainAction; import org.elasticsearch.action.explain.TransportExplainAction; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesAction; +import org.elasticsearch.action.fieldcaps.TransportFieldCapabilitiesAction; +import org.elasticsearch.action.fieldcaps.TransportFieldCapabilitiesIndexAction; import org.elasticsearch.action.fieldstats.FieldStatsAction; import org.elasticsearch.action.fieldstats.TransportFieldStatsAction; import org.elasticsearch.action.get.GetAction; @@ -205,6 +208,7 @@ import org.elasticsearch.plugins.ActionPlugin.ActionHandler; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.rest.action.RestFieldCapabilitiesAction; import org.elasticsearch.rest.action.RestFieldStatsAction; import org.elasticsearch.rest.action.RestMainAction; import org.elasticsearch.rest.action.admin.cluster.RestCancelTasksAction; @@ -479,6 +483,8 @@ public void reg actions.register(DeleteStoredScriptAction.INSTANCE, TransportDeleteStoredScriptAction.class); actions.register(FieldStatsAction.INSTANCE, TransportFieldStatsAction.class); + actions.register(FieldCapabilitiesAction.INSTANCE, TransportFieldCapabilitiesAction.class, + TransportFieldCapabilitiesIndexAction.class); actions.register(PutPipelineAction.INSTANCE, PutPipelineTransportAction.class); actions.register(GetPipelineAction.INSTANCE, GetPipelineTransportAction.class); @@ -587,6 +593,7 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestDeleteStoredScriptAction(settings, restController)); registerHandler.accept(new RestFieldStatsAction(settings, restController)); + registerHandler.accept(new RestFieldCapabilitiesAction(settings, restController)); // Tasks API registerHandler.accept(new RestListTasksAction(settings, restController, nodesInCluster)); diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java new file mode 100644 index 0000000000000..1ac78728b0d1e --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java @@ -0,0 +1,280 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; +import java.util.Comparator; + +/** + * Describes the capabilities of a field optionally merged across multiple indices. + */ +public class FieldCapabilities implements Writeable, ToXContent { + private final String name; + private final String type; + private final boolean isSearchable; + private final boolean isAggregatable; + + private final String[] indices; + private final String[] nonSearchableIndices; + private final String[] nonAggregatableIndices; + + /** + * Constructor + * @param name The name of the field. + * @param type The type associated with the field. + * @param isSearchable Whether this field is indexed for search. + * @param isAggregatable Whether this field can be aggregated on. + */ + FieldCapabilities(String name, String type, boolean isSearchable, boolean isAggregatable) { + this(name, type, isSearchable, isAggregatable, null, null, null); + } + + /** + * Constructor + * @param name The name of the field + * @param type The type associated with the field. + * @param isSearchable Whether this field is indexed for search. + * @param isAggregatable Whether this field can be aggregated on. + * @param indices The list of indices where this field name is defined as {@code type}, + * or null if all indices have the same {@code type} for the field. + * @param nonSearchableIndices The list of indices where this field is not searchable, + * or null if the field is searchable in all indices. + * @param nonAggregatableIndices The list of indices where this field is not aggregatable, + * or null if the field is aggregatable in all indices. + */ + FieldCapabilities(String name, String type, + boolean isSearchable, boolean isAggregatable, + String[] indices, + String[] nonSearchableIndices, + String[] nonAggregatableIndices) { + this.name = name; + this.type = type; + this.isSearchable = isSearchable; + this.isAggregatable = isAggregatable; + this.indices = indices; + this.nonSearchableIndices = nonSearchableIndices; + this.nonAggregatableIndices = nonAggregatableIndices; + } + + FieldCapabilities(StreamInput in) throws IOException { + this.name = in.readString(); + this.type = in.readString(); + this.isSearchable = in.readBoolean(); + this.isAggregatable = in.readBoolean(); + this.indices = in.readOptionalStringArray(); + this.nonSearchableIndices = in.readOptionalStringArray(); + this.nonAggregatableIndices = in.readOptionalStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeString(type); + out.writeBoolean(isSearchable); + out.writeBoolean(isAggregatable); + out.writeOptionalStringArray(indices); + out.writeOptionalStringArray(nonSearchableIndices); + out.writeOptionalStringArray(nonAggregatableIndices); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("type", type); + builder.field("searchable", isSearchable); + builder.field("aggregatable", isAggregatable); + if (indices != null) { + builder.field("indices", indices); + } + if (nonSearchableIndices != null) { + builder.field("non_searchable_indices", nonSearchableIndices); + } + if (nonAggregatableIndices != null) { + builder.field("non_aggregatable_indices", nonAggregatableIndices); + } + builder.endObject(); + return builder; + } + + /** + * The name of the field. + */ + public String getName() { + return name; + } + + /** + * Whether this field is indexed for search on all indices. + */ + public boolean isAggregatable() { + return isAggregatable; + } + + /** + * Whether this field can be aggregated on all indices. + */ + public boolean isSearchable() { + return isSearchable; + } + + /** + * The type of the field. + */ + public String getType() { + return type; + } + + /** + * The list of indices where this field name is defined as {@code type}, + * or null if all indices have the same {@code type} for the field. + */ + public String[] indices() { + return indices; + } + + /** + * The list of indices where this field is not searchable, + * or null if the field is searchable in all indices. + */ + public String[] nonSearchableIndices() { + return nonSearchableIndices; + } + + /** + * The list of indices where this field is not aggregatable, + * or null if the field is aggregatable in all indices. + */ + public String[] nonAggregatableIndices() { + return nonAggregatableIndices; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FieldCapabilities that = (FieldCapabilities) o; + + if (isSearchable != that.isSearchable) return false; + if (isAggregatable != that.isAggregatable) return false; + if (!name.equals(that.name)) return false; + if (!type.equals(that.type)) return false; + if (!Arrays.equals(indices, that.indices)) return false; + if (!Arrays.equals(nonSearchableIndices, that.nonSearchableIndices)) return false; + return Arrays.equals(nonAggregatableIndices, that.nonAggregatableIndices); + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + (isSearchable ? 1 : 0); + result = 31 * result + (isAggregatable ? 1 : 0); + result = 31 * result + Arrays.hashCode(indices); + result = 31 * result + Arrays.hashCode(nonSearchableIndices); + result = 31 * result + Arrays.hashCode(nonAggregatableIndices); + return result; + } + + static class Builder { + private String name; + private String type; + private boolean isSearchable; + private boolean isAggregatable; + private List indiceList; + + Builder(String name, String type) { + this.name = name; + this.type = type; + this.isSearchable = true; + this.isAggregatable = true; + this.indiceList = new ArrayList<>(); + } + + void add(String index, boolean search, boolean agg) { + IndexCaps indexCaps = new IndexCaps(index, search, agg); + indiceList.add(indexCaps); + this.isSearchable &= search; + this.isAggregatable &= agg; + } + + FieldCapabilities build(boolean withIndices) { + final String[] indices; + Collections.sort(indiceList, Comparator.comparing(o -> o.name)); + if (withIndices) { + indices = indiceList.stream() + .map(caps -> caps.name) + .toArray(String[]::new); + } else { + indices = null; + } + + final String[] nonSearchableIndices; + if (isSearchable == false && + indiceList.stream().anyMatch((caps) -> caps.isSearchable)) { + // Iff this field is searchable in some indices AND non-searchable in others + // we record the list of non-searchable indices + nonSearchableIndices = indiceList.stream() + .filter((caps) -> caps.isSearchable == false) + .map(caps -> caps.name) + .toArray(String[]::new); + } else { + nonSearchableIndices = null; + } + + final String[] nonAggregatableIndices; + if (isAggregatable == false && + indiceList.stream().anyMatch((caps) -> caps.isAggregatable)) { + // Iff this field is aggregatable in some indices AND non-searchable in others + // we keep the list of non-aggregatable indices + nonAggregatableIndices = indiceList.stream() + .filter((caps) -> caps.isAggregatable == false) + .map(caps -> caps.name) + .toArray(String[]::new); + } else { + nonAggregatableIndices = null; + } + return new FieldCapabilities(name, type, isSearchable, isAggregatable, + indices, nonSearchableIndices, nonAggregatableIndices); + } + } + + private static class IndexCaps { + final String name; + final boolean isSearchable; + final boolean isAggregatable; + + IndexCaps(String name, boolean isSearchable, boolean isAggregatable) { + this.name = name; + this.isSearchable = isSearchable; + this.isAggregatable = isAggregatable; + } + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesAction.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesAction.java new file mode 100644 index 0000000000000..93d67f3fc3cc4 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesAction.java @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +public class FieldCapabilitiesAction extends Action { + + public static final FieldCapabilitiesAction INSTANCE = new FieldCapabilitiesAction(); + public static final String NAME = "indices:data/read/field_caps"; + + private FieldCapabilitiesAction() { + super(NAME); + } + + @Override + public FieldCapabilitiesResponse newResponse() { + return new FieldCapabilitiesResponse(); + } + + @Override + public FieldCapabilitiesRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new FieldCapabilitiesRequestBuilder(client, this); + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexRequest.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexRequest.java new file mode 100644 index 0000000000000..460a21ae866aa --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexRequest.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.single.shard.SingleShardRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +public class FieldCapabilitiesIndexRequest + extends SingleShardRequest { + + private String[] fields; + + // For serialization + FieldCapabilitiesIndexRequest() {} + + FieldCapabilitiesIndexRequest(String[] fields, String index) { + super(index); + if (fields == null || fields.length == 0) { + throw new IllegalArgumentException("specified fields can't be null or empty"); + } + this.fields = fields; + } + + public String[] fields() { + return fields; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + fields = in.readStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringArray(fields); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java new file mode 100644 index 0000000000000..de520ee6274f6 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Map; + +/** + * Response for {@link FieldCapabilitiesIndexRequest} requests. + */ +public class FieldCapabilitiesIndexResponse extends ActionResponse { + private String indexName; + private Map responseMap; + + FieldCapabilitiesIndexResponse(String indexName, Map responseMap) { + this.indexName = indexName; + this.responseMap = responseMap; + } + + FieldCapabilitiesIndexResponse() { + } + + + /** + * Get the index name + */ + public String getIndexName() { + return indexName; + } + + /** + * Get the field capabilities map + */ + public Map get() { + return responseMap; + } + + /** + * + * Get the field capabilities for the provided {@code field} + */ + public FieldCapabilities getField(String field) { + return responseMap.get(field); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.indexName = in.readString(); + this.responseMap = + in.readMap(StreamInput::readString, FieldCapabilities::new); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(indexName); + out.writeMap(responseMap, + StreamOutput::writeString, (valueOut, fc) -> fc.writeTo(valueOut)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FieldCapabilitiesIndexResponse that = (FieldCapabilitiesIndexResponse) o; + + return responseMap.equals(that.responseMap); + } + + @Override + public int hashCode() { + return responseMap.hashCode(); + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java new file mode 100644 index 0000000000000..53a6c14a8e914 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -0,0 +1,143 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.ValidateActions; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.elasticsearch.common.xcontent.ObjectParser.fromList; + +public class FieldCapabilitiesRequest extends ActionRequest implements IndicesRequest { + public static final ParseField FIELDS_FIELD = new ParseField("fields"); + public static final String NAME = "field_caps_request"; + private String[] indices = Strings.EMPTY_ARRAY; + private IndicesOptions indicesOptions = IndicesOptions.strictExpandOpen(); + private String[] fields = Strings.EMPTY_ARRAY; + + private static ObjectParser PARSER = + new ObjectParser<>(NAME, FieldCapabilitiesRequest::new); + + static { + PARSER.declareStringArray(fromList(String.class, FieldCapabilitiesRequest::fields), + FIELDS_FIELD); + } + + public FieldCapabilitiesRequest() {} + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + fields = in.readStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringArray(fields); + } + + public static FieldCapabilitiesRequest parseFields(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + /** + * The list of field names to retrieve + */ + public FieldCapabilitiesRequest fields(String... fields) { + if (fields == null || fields.length == 0) { + throw new IllegalArgumentException("specified fields can't be null or empty"); + } + Set fieldSet = new HashSet<>(Arrays.asList(fields)); + this.fields = fieldSet.toArray(new String[0]); + return this; + } + + public String[] fields() { + return fields; + } + + /** + * + * The list of indices to lookup + */ + public FieldCapabilitiesRequest indices(String[] indices) { + this.indices = indices; + return this; + } + + public FieldCapabilitiesRequest indicesOptions(IndicesOptions indicesOptions) { + this.indicesOptions = indicesOptions; + return this; + } + + @Override + public String[] indices() { + return indices; + } + + @Override + public IndicesOptions indicesOptions() { + return indicesOptions; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (fields == null || fields.length == 0) { + validationException = + ValidateActions.addValidationError("no fields specified", validationException); + } + return validationException; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FieldCapabilitiesRequest that = (FieldCapabilitiesRequest) o; + + if (!Arrays.equals(indices, that.indices)) return false; + if (!indicesOptions.equals(that.indicesOptions)) return false; + return Arrays.equals(fields, that.fields); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(indices); + result = 31 * result + indicesOptions.hashCode(); + result = 31 * result + Arrays.hashCode(fields); + return result; + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestBuilder.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestBuilder.java new file mode 100644 index 0000000000000..742d5b3ee3297 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestBuilder.java @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; + +public class FieldCapabilitiesRequestBuilder extends + ActionRequestBuilder { + public FieldCapabilitiesRequestBuilder(ElasticsearchClient client, + FieldCapabilitiesAction action, + String... indices) { + super(client, action, new FieldCapabilitiesRequest().indices(indices)); + } + + /** + * The list of field names to retrieve. + */ + public FieldCapabilitiesRequestBuilder setFields(String... fields) { + request().fields(fields); + return this; + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java new file mode 100644 index 0000000000000..9ff2cf3850b1f --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * Response for {@link FieldCapabilitiesRequest} requests. + */ +public class FieldCapabilitiesResponse extends ActionResponse implements ToXContent { + private Map> responseMap; + + FieldCapabilitiesResponse(Map> responseMap) { + this.responseMap = responseMap; + } + + /** + * Used for serialization + */ + FieldCapabilitiesResponse() { + this.responseMap = Collections.emptyMap(); + } + + /** + * Get the field capabilities map. + */ + public Map> get() { + return responseMap; + } + + /** + * + * Get the field capabilities per type for the provided {@code field}. + */ + public Map getField(String field) { + return responseMap.get(field); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + this.responseMap = + in.readMap(StreamInput::readString, FieldCapabilitiesResponse::readField); + } + + private static Map readField(StreamInput in) throws IOException { + return in.readMap(StreamInput::readString, FieldCapabilities::new); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeMap(responseMap, StreamOutput::writeString, FieldCapabilitiesResponse::writeField); + } + + private static void writeField(StreamOutput out, + Map map) throws IOException { + out.writeMap(map, StreamOutput::writeString, (valueOut, fc) -> fc.writeTo(valueOut)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("fields", responseMap); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FieldCapabilitiesResponse that = (FieldCapabilitiesResponse) o; + + return responseMap.equals(that.responseMap); + } + + @Override + public int hashCode() { + return responseMap.hashCode(); + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java new file mode 100644 index 0000000000000..a7f268eaf5d8d --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +public class TransportFieldCapabilitiesAction + extends HandledTransportAction { + private final ClusterService clusterService; + private final TransportFieldCapabilitiesIndexAction shardAction; + + @Inject + public TransportFieldCapabilitiesAction(Settings settings, TransportService transportService, + ClusterService clusterService, ThreadPool threadPool, + TransportFieldCapabilitiesIndexAction shardAction, + ActionFilters actionFilters, + IndexNameExpressionResolver + indexNameExpressionResolver) { + super(settings, FieldCapabilitiesAction.NAME, threadPool, transportService, + actionFilters, indexNameExpressionResolver, FieldCapabilitiesRequest::new); + this.clusterService = clusterService; + this.shardAction = shardAction; + } + + @Override + protected void doExecute(FieldCapabilitiesRequest request, + final ActionListener listener) { + ClusterState clusterState = clusterService.state(); + String[] concreteIndices = + indexNameExpressionResolver.concreteIndexNames(clusterState, request); + final AtomicInteger indexCounter = new AtomicInteger(); + final AtomicInteger completionCounter = new AtomicInteger(concreteIndices.length); + final AtomicReferenceArray indexResponses = + new AtomicReferenceArray<>(concreteIndices.length); + if (concreteIndices.length == 0) { + listener.onResponse(new FieldCapabilitiesResponse()); + } else { + for (String index : concreteIndices) { + FieldCapabilitiesIndexRequest indexRequest = + new FieldCapabilitiesIndexRequest(request.fields(), index); + shardAction.execute(indexRequest, + new ActionListener () { + @Override + public void onResponse(FieldCapabilitiesIndexResponse result) { + indexResponses.set(indexCounter.getAndIncrement(), result); + if (completionCounter.decrementAndGet() == 0) { + listener.onResponse(merge(indexResponses)); + } + } + + @Override + public void onFailure(Exception e) { + indexResponses.set(indexCounter.getAndIncrement(), e); + if (completionCounter.decrementAndGet() == 0) { + listener.onResponse(merge(indexResponses)); + } + } + }); + } + } + } + + private FieldCapabilitiesResponse merge(AtomicReferenceArray indexResponses) { + Map> responseMapBuilder = new HashMap<> (); + for (int i = 0; i < indexResponses.length(); i++) { + Object element = indexResponses.get(i); + if (element instanceof FieldCapabilitiesIndexResponse == false) { + assert element instanceof Exception; + continue; + } + FieldCapabilitiesIndexResponse response = (FieldCapabilitiesIndexResponse) element; + for (String field : response.get().keySet()) { + Map typeMap = responseMapBuilder.get(field); + if (typeMap == null) { + typeMap = new HashMap<> (); + responseMapBuilder.put(field, typeMap); + } + FieldCapabilities fieldCap = response.getField(field); + FieldCapabilities.Builder builder = typeMap.get(fieldCap.getType()); + if (builder == null) { + builder = new FieldCapabilities.Builder(field, fieldCap.getType()); + typeMap.put(fieldCap.getType(), builder); + } + builder.add(response.getIndexName(), + fieldCap.isSearchable(), fieldCap.isAggregatable()); + } + } + + Map> responseMap = new HashMap<>(); + for (Map.Entry> entry : + responseMapBuilder.entrySet()) { + Map typeMap = new HashMap<>(); + boolean multiTypes = entry.getValue().size() > 1; + for (Map.Entry fieldEntry : + entry.getValue().entrySet()) { + typeMap.put(fieldEntry.getKey(), fieldEntry.getValue().build(multiTypes)); + } + responseMap.put(entry.getKey(), typeMap); + } + + return new FieldCapabilitiesResponse(responseMap); + } +} diff --git a/core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java b/core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java new file mode 100644 index 0000000000000..5bab727686015 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.single.shard.TransportSingleShardAction; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlockException; +import org.elasticsearch.cluster.block.ClusterBlockLevel; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.routing.ShardsIterator; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.indices.IndicesService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class TransportFieldCapabilitiesIndexAction + extends TransportSingleShardAction { + + private static final String ACTION_NAME = FieldCapabilitiesAction.NAME + "[index]"; + + protected final ClusterService clusterService; + private final IndicesService indicesService; + + @Inject + public TransportFieldCapabilitiesIndexAction(Settings settings, + ClusterService clusterService, + TransportService transportService, + IndicesService indicesService, + ThreadPool threadPool, + ActionFilters actionFilters, + IndexNameExpressionResolver + indexNameExpressionResolver) { + super(settings, + ACTION_NAME, + threadPool, + clusterService, + transportService, + actionFilters, + indexNameExpressionResolver, + FieldCapabilitiesIndexRequest::new, + ThreadPool.Names.MANAGEMENT); + this.clusterService = clusterService; + this.indicesService = indicesService; + } + + @Override + protected boolean resolveIndex(FieldCapabilitiesIndexRequest request) { + //internal action, index already resolved + return false; + } + + @Override + protected ShardsIterator shards(ClusterState state, InternalRequest request) { + // Will balance requests between shards + // Resolve patterns and deduplicate + return state.routingTable().index(request.concreteIndex()).randomAllActiveShardsIt(); + } + + @Override + protected FieldCapabilitiesIndexResponse shardOperation( + final FieldCapabilitiesIndexRequest request, + ShardId shardId) { + MapperService mapperService = + indicesService.indexServiceSafe(shardId.getIndex()).mapperService(); + Set fieldNames = new HashSet<>(); + for (String field : request.fields()) { + fieldNames.addAll(mapperService.simpleMatchToIndexNames(field)); + } + Map responseMap = new HashMap<>(); + for (String field : fieldNames) { + MappedFieldType ft = mapperService.fullName(field); + FieldCapabilities fieldCap = new FieldCapabilities(field, + ft.typeName(), + ft.isSearchable(), + ft.isAggregatable()); + responseMap.put(field, fieldCap); + } + return new FieldCapabilitiesIndexResponse(shardId.getIndexName(), responseMap); + } + + @Override + protected FieldCapabilitiesIndexResponse newResponse() { + return new FieldCapabilitiesIndexResponse(); + } + + @Override + protected ClusterBlockException checkRequestBlock(ClusterState state, + InternalRequest request) { + return state.blocks().indexBlockedException(ClusterBlockLevel.METADATA_READ, + request.concreteIndex()); + } +} diff --git a/core/src/main/java/org/elasticsearch/client/Client.java b/core/src/main/java/org/elasticsearch/client/Client.java index 0cf22d7a2c4fc..663b820dc3956 100644 --- a/core/src/main/java/org/elasticsearch/client/Client.java +++ b/core/src/main/java/org/elasticsearch/client/Client.java @@ -30,6 +30,10 @@ import org.elasticsearch.action.explain.ExplainRequest; import org.elasticsearch.action.explain.ExplainRequestBuilder; import org.elasticsearch.action.explain.ExplainResponse; +import org.elasticsearch.action.fieldcaps.FieldCapabilities; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequestBuilder; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; import org.elasticsearch.action.fieldstats.FieldStatsRequest; import org.elasticsearch.action.fieldstats.FieldStatsRequestBuilder; import org.elasticsearch.action.fieldstats.FieldStatsResponse; @@ -458,6 +462,21 @@ public interface Client extends ElasticsearchClient, Releasable { void fieldStats(FieldStatsRequest request, ActionListener listener); + /** + * Builder for the field capabilities request. + */ + FieldCapabilitiesRequestBuilder prepareFieldCaps(); + + /** + * An action that returns the field capabilities from the provided request + */ + ActionFuture fieldCaps(FieldCapabilitiesRequest request); + + /** + * An action that returns the field capabilities from the provided request + */ + void fieldCaps(FieldCapabilitiesRequest request, ActionListener listener); + /** * Returns this clients settings */ diff --git a/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java b/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java index 03fdee2db3400..b254039910c01 100644 --- a/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java +++ b/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java @@ -50,6 +50,9 @@ import org.elasticsearch.action.admin.indices.exists.types.TypesExistsRequest; import org.elasticsearch.action.admin.indices.exists.types.TypesExistsRequestBuilder; import org.elasticsearch.action.admin.indices.exists.types.TypesExistsResponse; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequestBuilder; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; import org.elasticsearch.action.admin.indices.flush.FlushRequest; import org.elasticsearch.action.admin.indices.flush.FlushRequestBuilder; import org.elasticsearch.action.admin.indices.flush.FlushResponse; @@ -817,5 +820,4 @@ public interface IndicesAdminClient extends ElasticsearchClient { * Swaps the index pointed to by an alias given all provided conditions are satisfied */ void rolloverIndex(RolloverRequest request, ActionListener listener); - } diff --git a/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java b/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java index e0ebcfe70a924..726875a6d5c20 100644 --- a/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java +++ b/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java @@ -272,6 +272,10 @@ import org.elasticsearch.action.explain.ExplainRequest; import org.elasticsearch.action.explain.ExplainRequestBuilder; import org.elasticsearch.action.explain.ExplainResponse; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesAction; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequestBuilder; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; import org.elasticsearch.action.fieldstats.FieldStatsAction; import org.elasticsearch.action.fieldstats.FieldStatsRequest; import org.elasticsearch.action.fieldstats.FieldStatsRequestBuilder; @@ -667,6 +671,21 @@ public FieldStatsRequestBuilder prepareFieldStats() { return new FieldStatsRequestBuilder(this, FieldStatsAction.INSTANCE); } + @Override + public void fieldCaps(FieldCapabilitiesRequest request, ActionListener listener) { + execute(FieldCapabilitiesAction.INSTANCE, request, listener); + } + + @Override + public ActionFuture fieldCaps(FieldCapabilitiesRequest request) { + return execute(FieldCapabilitiesAction.INSTANCE, request); + } + + @Override + public FieldCapabilitiesRequestBuilder prepareFieldCaps() { + return new FieldCapabilitiesRequestBuilder(this, FieldCapabilitiesAction.INSTANCE); + } + static class Admin implements AdminClient { private final ClusterAdmin clusterAdmin; diff --git a/core/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java b/core/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java index 93f22d42a8080..55c2e4cb3c698 100644 --- a/core/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java +++ b/core/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java @@ -313,14 +313,14 @@ public Object valueForDisplay(Object value) { /** Returns true if the field is searchable. * */ - protected boolean isSearchable() { + public boolean isSearchable() { return indexOptions() != IndexOptions.NONE; } /** Returns true if the field is aggregatable. * */ - protected boolean isAggregatable() { + public boolean isAggregatable() { try { fielddataBuilder(); return true; diff --git a/core/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java b/core/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java new file mode 100644 index 0000000000000..e983bdc182a01 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.rest.action; + +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; + +import java.io.IOException; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.rest.RestStatus.NOT_FOUND; +import static org.elasticsearch.rest.RestStatus.OK; + +public class RestFieldCapabilitiesAction extends BaseRestHandler { + public RestFieldCapabilitiesAction(Settings settings, RestController controller) { + super(settings); + controller.registerHandler(GET, "/_field_caps", this); + controller.registerHandler(POST, "/_field_caps", this); + controller.registerHandler(GET, "/{index}/_field_caps", this); + controller.registerHandler(POST, "/{index}/_field_caps", this); + } + + @Override + public RestChannelConsumer prepareRequest(final RestRequest request, + final NodeClient client) throws IOException { + if (request.hasContentOrSourceParam() && request.hasParam("fields")) { + throw new IllegalArgumentException("can't specify a request body and [fields]" + + " request parameter, either specify a request body or the" + + " [fields] request parameter"); + } + final String[] indices = Strings.splitStringByCommaToArray(request.param("index")); + final FieldCapabilitiesRequest fieldRequest; + if (request.hasContentOrSourceParam()) { + try (XContentParser parser = request.contentOrSourceParamParser()) { + fieldRequest = FieldCapabilitiesRequest.parseFields(parser); + } + } else { + fieldRequest = new FieldCapabilitiesRequest(); + fieldRequest.fields(Strings.splitStringByCommaToArray(request.param("fields"))); + } + fieldRequest.indices(indices); + fieldRequest.indicesOptions( + IndicesOptions.fromRequest(request, fieldRequest.indicesOptions()) + ); + return channel -> client.fieldCaps(fieldRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(FieldCapabilitiesResponse response, + XContentBuilder builder) throws Exception { + RestStatus status = OK; + builder.startObject(); + response.toXContent(builder, request); + builder.endObject(); + return new BytesRestResponse(status, builder); + } + }); + } +} diff --git a/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestTests.java b/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestTests.java new file mode 100644 index 0000000000000..abc89e356259e --- /dev/null +++ b/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestTests.java @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +public class FieldCapabilitiesRequestTests extends ESTestCase { + private FieldCapabilitiesRequest randomRequest() { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest(); + int size = randomIntBetween(1, 20); + String[] randomFields = new String[size]; + for (int i = 0; i < size; i++) { + randomFields[i] = randomAsciiOfLengthBetween(5, 10); + } + request.fields(randomFields); + return request; + } + + public void testFieldCapsRequestSerialization() throws IOException { + for (int i = 0; i < 20; i++) { + FieldCapabilitiesRequest request = randomRequest(); + BytesStreamOutput output = new BytesStreamOutput(); + request.writeTo(output); + output.flush(); + StreamInput input = output.bytes().streamInput(); + FieldCapabilitiesRequest deserialized = new FieldCapabilitiesRequest(); + deserialized.readFrom(input); + assertEquals(deserialized, request); + assertEquals(deserialized.hashCode(), request.hashCode()); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java b/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java new file mode 100644 index 0000000000000..8d64f9a538c56 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class FieldCapabilitiesResponseTests extends ESTestCase { + private FieldCapabilitiesResponse randomResponse() { + Map > fieldMap = new HashMap<> (); + int numFields = randomInt(10); + for (int i = 0; i < numFields; i++) { + String fieldName = randomAsciiOfLengthBetween(5, 10); + int numIndices = randomIntBetween(1, 5); + Map indexFieldMap = new HashMap<> (); + for (int j = 0; j < numIndices; j++) { + String index = randomAsciiOfLengthBetween(10, 20); + indexFieldMap.put(index, FieldCapabilitiesTests.randomFieldCaps()); + } + fieldMap.put(fieldName, indexFieldMap); + } + return new FieldCapabilitiesResponse(fieldMap); + } + + public void testSerialization() throws IOException { + for (int i = 0; i < 20; i++) { + FieldCapabilitiesResponse response = randomResponse(); + BytesStreamOutput output = new BytesStreamOutput(); + response.writeTo(output); + output.flush(); + StreamInput input = output.bytes().streamInput(); + FieldCapabilitiesResponse deserialized = new FieldCapabilitiesResponse(); + deserialized.readFrom(input); + assertEquals(deserialized, response); + assertEquals(deserialized.hashCode(), response.hashCode()); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java b/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java new file mode 100644 index 0000000000000..a1df6b6529b8a --- /dev/null +++ b/core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java @@ -0,0 +1,109 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.fieldcaps; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import static org.hamcrest.Matchers.equalTo; + +public class FieldCapabilitiesTests extends AbstractWireSerializingTestCase { + @Override + protected FieldCapabilities createTestInstance() { + return randomFieldCaps(); + } + + @Override + protected Writeable.Reader instanceReader() { + return FieldCapabilities::new; + } + + public void testBuilder() { + FieldCapabilities.Builder builder = new FieldCapabilities.Builder("field", "type"); + builder.add("index1", true, false); + builder.add("index2", true, false); + builder.add("index3", true, false); + + { + FieldCapabilities cap1 = builder.build(false); + assertThat(cap1.isSearchable(), equalTo(true)); + assertThat(cap1.isAggregatable(), equalTo(false)); + assertNull(cap1.indices()); + assertNull(cap1.nonSearchableIndices()); + assertNull(cap1.nonAggregatableIndices()); + + FieldCapabilities cap2 = builder.build(true); + assertThat(cap2.isSearchable(), equalTo(true)); + assertThat(cap2.isAggregatable(), equalTo(false)); + assertThat(cap2.indices().length, equalTo(3)); + assertThat(cap2.indices(), equalTo(new String[]{"index1", "index2", "index3"})); + assertNull(cap2.nonSearchableIndices()); + assertNull(cap2.nonAggregatableIndices()); + } + + builder = new FieldCapabilities.Builder("field", "type"); + builder.add("index1", false, true); + builder.add("index2", true, false); + builder.add("index3", false, false); + { + FieldCapabilities cap1 = builder.build(false); + assertThat(cap1.isSearchable(), equalTo(false)); + assertThat(cap1.isAggregatable(), equalTo(false)); + assertNull(cap1.indices()); + assertThat(cap1.nonSearchableIndices(), equalTo(new String[]{"index1", "index3"})); + assertThat(cap1.nonAggregatableIndices(), equalTo(new String[]{"index2", "index3"})); + + FieldCapabilities cap2 = builder.build(true); + assertThat(cap2.isSearchable(), equalTo(false)); + assertThat(cap2.isAggregatable(), equalTo(false)); + assertThat(cap2.indices().length, equalTo(3)); + assertThat(cap2.indices(), equalTo(new String[]{"index1", "index2", "index3"})); + assertThat(cap1.nonSearchableIndices(), equalTo(new String[]{"index1", "index3"})); + assertThat(cap1.nonAggregatableIndices(), equalTo(new String[]{"index2", "index3"})); + } + } + + static FieldCapabilities randomFieldCaps() { + String[] indices = null; + if (randomBoolean()) { + indices = new String[randomIntBetween(1, 5)]; + for (int i = 0; i < indices.length; i++) { + indices[i] = randomAsciiOfLengthBetween(5, 20); + } + } + String[] nonSearchableIndices = null; + if (randomBoolean()) { + nonSearchableIndices = new String[randomIntBetween(0, 5)]; + for (int i = 0; i < nonSearchableIndices.length; i++) { + nonSearchableIndices[i] = randomAsciiOfLengthBetween(5, 20); + } + } + String[] nonAggregatableIndices = null; + if (randomBoolean()) { + nonAggregatableIndices = new String[randomIntBetween(0, 5)]; + for (int i = 0; i < nonAggregatableIndices.length; i++) { + nonAggregatableIndices[i] = randomAsciiOfLengthBetween(5, 20); + } + } + return new FieldCapabilities(randomAsciiOfLengthBetween(5, 20), + randomAsciiOfLengthBetween(5, 20), randomBoolean(), randomBoolean(), + indices, nonSearchableIndices, nonAggregatableIndices); + } +} diff --git a/docs/reference/search/field-caps.asciidoc b/docs/reference/search/field-caps.asciidoc new file mode 100644 index 0000000000000..d327362f81c7b --- /dev/null +++ b/docs/reference/search/field-caps.asciidoc @@ -0,0 +1,126 @@ +[[search-field-caps]] +== Field Capabilities API + +experimental[] + +The field capabilities API allows to retrieve the capabilities of fields among multiple indices. + +The field capabilities api by default executes on all indices: + +[source,js] +-------------------------------------------------- +GET _field_caps?fields=rating +-------------------------------------------------- +// CONSOLE + +... but the request can also be restricted to specific indices: + +[source,js] +-------------------------------------------------- +GET twitter/_field_caps?fields=rating +-------------------------------------------------- +// CONSOLE +// TEST[setup:twitter] + +Alternatively the `fields` option can also be defined in the request body: + +[source,js] +-------------------------------------------------- +POST _field_caps +{ + "fields" : ["rating"] +} +-------------------------------------------------- +// CONSOLE + +This is equivalent to the previous request. + +Supported request options: + +[horizontal] +`fields`:: A list of fields to compute stats for. The field name supports wildcard notation. For example, using `text_*` + will cause all fields that match the expression to be returned. + +[float] +=== Field Capabilities + +The field capabilities api returns the following information per field: + +[horizontal] +`is_searchable`:: + +Whether this field is indexed for search on all indices. + +`is_aggregatable`:: + +Whether this field can be aggregated on all indices. + +`indices`:: + +The list of indices where this field has the same type, +or null if all indices have the same type for the field. + +`non_searchable_indices`:: + +The list of indices where this field is not searchable, +or null if all indices have the same definition for the field. + +`non_aggregatable_indices`:: + +The list of indices where this field is not aggregatable, +or null if all indices have the same definition for the field. + + +[float] +=== Response format + +Request: + +[source,js] +-------------------------------------------------- +GET _field_caps?fields=rating,title +-------------------------------------------------- +// CONSOLE + +[source,js] +-------------------------------------------------- +{ + "fields": { + "rating": { <1> + "long": { + "is_searchable": true, + "is_aggregatable": false, + "indices": ["index1", "index2"], + "non_aggregatable_indices": ["index1"] <2> + }, + "keyword": { + "is_searchable": false, + "is_aggregatable": true, + "indices": ["index3", "index4"], + "non_searchable_indices": ["index4"] <3> + } + }, + "title": { <4> + "text": { + "is_searchable": true, + "is_aggregatable": false + + } + } + } +} +-------------------------------------------------- +// NOTCONSOLE + +<1> The field `rating` is defined as a long in `index1` and `index2` +and as a `keyword` in `index3` and `index4`. +<2> The field `rating` is not aggregatable in `index1`. +<3> The field `rating` is not searchable in `index4`. +<4> The field `title` is defined as `text` in all indices. + + + + + + + diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/field_caps.json b/rest-api-spec/src/main/resources/rest-api-spec/api/field_caps.json new file mode 100644 index 0000000000000..d993dc0545b74 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/field_caps.json @@ -0,0 +1,43 @@ +{ + "field_caps": { + "documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/master/search-field-caps.html", + "methods": ["GET", "POST"], + "url": { + "path": "/_field_caps", + "paths": [ + "/_field_caps", + "/{index}/_field_caps" + ], + "parts": { + "index": { + "type" : "list", + "description" : "A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices" + } + }, + "params": { + "fields": { + "type" : "list", + "description" : "A comma-separated list of field names" + }, + "ignore_unavailable": { + "type" : "boolean", + "description" : "Whether specified concrete indices should be ignored when unavailable (missing or closed)" + }, + "allow_no_indices": { + "type" : "boolean", + "description" : "Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified)" + }, + "expand_wildcards": { + "type" : "enum", + "options" : ["open","closed","none","all"], + "default" : "open", + "description" : "Whether to expand wildcard expression to concrete indices that are open, closed or both." + } + } + }, + "body": { + "description": "Field json objects containing an array of field names", + "required": false + } + } +} \ No newline at end of file diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yaml new file mode 100644 index 0000000000000..edda7b6dbf3d0 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yaml @@ -0,0 +1,167 @@ +--- +setup: + - do: + indices.create: + index: test1 + body: + mappings: + t: + properties: + text: + type: text + keyword: + type: keyword + number: + type: double + geo: + type: geo_point + object: + type: object + properties: + nested1 : + type : text + index: false + nested2: + type: float + doc_values: false + - do: + indices.create: + index: test2 + body: + mappings: + t: + properties: + text: + type: text + keyword: + type: keyword + number: + type: double + geo: + type: geo_point + object: + type: object + properties: + nested1 : + type : text + index: true + nested2: + type: float + doc_values: true + - do: + indices.create: + index: test3 + body: + mappings: + t: + properties: + text: + type: text + keyword: + type: keyword + number: + type: long + geo: + type: keyword + object: + type: object + properties: + nested1 : + type : long + index: false + nested2: + type: keyword + doc_values: false + +--- +"Get simple field caps": + - skip: + version: " - 5.99.99" + reason: this uses a new API that has been added in 6.0 + + - do: + field_caps: + index: 'test1,test2,test3' + fields: [text, keyword, number, geo] + + - match: {fields.text.text.searchable: true} + - match: {fields.text.text.aggregatable: false} + - is_false: fields.text.text.indices + - is_false: fields.text.text.non_searchable_indices + - is_false: fields.text.text.non_aggregatable_indices + - match: {fields.keyword.keyword.searchable: true} + - match: {fields.keyword.keyword.aggregatable: true} + - is_false: fields.text.keyword.indices + - is_false: fields.text.keyword.non_searchable_indices + - is_false: fields.text.keyword.non_aggregatable_indices + - match: {fields.number.double.searchable: true} + - match: {fields.number.double.aggregatable: true} + - match: {fields.number.double.indices: ["test1", "test2"]} + - is_false: fields.number.double.non_searchable_indices + - is_false: fields.number.double.non_aggregatable_indices + - match: {fields.number.long.searchable: true} + - match: {fields.number.long.aggregatable: true} + - match: {fields.number.long.indices: ["test3"]} + - is_false: fields.number.long.non_searchable_indices + - is_false: fields.number.long.non_aggregatable_indices + - match: {fields.geo.geo_point.searchable: true} + - match: {fields.geo.geo_point.aggregatable: true} + - match: {fields.geo.geo_point.indices: ["test1", "test2"]} + - is_false: fields.geo.geo_point.non_searchable_indices + - is_false: fields.geo.geo_point.non_aggregatable_indices + - match: {fields.geo.keyword.searchable: true} + - match: {fields.geo.keyword.aggregatable: true} + - match: {fields.geo.keyword.indices: ["test3"]} + - is_false: fields.geo.keyword.non_searchable_indices + - is_false: fields.geo.keyword.on_aggregatable_indices +--- +"Get nested field caps": + - skip: + version: " - 5.99.99" + reason: this uses a new API that has been added in 6.0 + + - do: + field_caps: + index: 'test1,test2,test3' + fields: object* + + - match: {fields.object\.nested1.long.searchable: false} + - match: {fields.object\.nested1.long.aggregatable: true} + - match: {fields.object\.nested1.long.indices: ["test3"]} + - is_false: fields.object\.nested1.long.non_searchable_indices + - is_false: fields.object\.nested1.long.non_aggregatable_indices + - match: {fields.object\.nested1.text.searchable: false} + - match: {fields.object\.nested1.text.aggregatable: false} + - match: {fields.object\.nested1.text.indices: ["test1", "test2"]} + - match: {fields.object\.nested1.text.non_searchable_indices: ["test1"]} + - is_false: fields.object\.nested1.text.non_aggregatable_indices + - match: {fields.object\.nested2.float.searchable: true} + - match: {fields.object\.nested2.float.aggregatable: false} + - match: {fields.object\.nested2.float.indices: ["test1", "test2"]} + - match: {fields.object\.nested2.float.non_aggregatable_indices: ["test1"]} + - is_false: fields.object\.nested2.float.non_searchable_indices + - match: {fields.object\.nested2.keyword.searchable: true} + - match: {fields.object\.nested2.keyword.aggregatable: false} + - match: {fields.object\.nested2.keyword.indices: ["test3"]} + - is_false: fields.object\.nested2.keyword.non_aggregatable_indices + - is_false: fields.object\.nested2.keyword.non_searchable_indices +--- +"Get prefix field caps": + - skip: + version: " - 5.99.99" + reason: this uses a new API that has been added in 6.0 + + - do: + field_caps: + index: _all + fields: "n*" + - match: {fields.number.double.searchable: true} + - match: {fields.number.double.aggregatable: true} + - match: {fields.number.double.indices: ["test1", "test2"]} + - is_false: fields.number.double.non_searchable_indices + - is_false: fields.number.double.non_aggregatable_indices + - match: {fields.number.long.searchable: true} + - match: {fields.number.long.aggregatable: true} + - match: {fields.number.long.indices: ["test3"]} + - is_false: fields.number.long.non_searchable_indices + - is_false: fields.number.long.non_aggregatable_indices