Browse Source

Add FieldCapabilities (_field_caps) API (#23007)

This change introduces a new API called `_field_caps` that allows to retrieve the capabilities of specific fields.

Example:

````
GET t,s,v,w/_field_caps?fields=field1,field2
````
... returns:
````
{
   "fields": {
      "field1": {
         "string": {
            "searchable": true,
            "aggregatable": true
         }
      },
      "field2": {
         "keyword": {
            "searchable": false,
            "aggregatable": true,
            "non_searchable_indices": ["t"]
            "indices": ["t", "s"]
         },
         "long": {
            "searchable": true,
            "aggregatable": false,
            "non_aggregatable_indices": ["v"]
            "indices": ["v", "w"]
         }
      }
   }
}
````

In this example `field1` have the same type `text` across the requested indices `t`, `s`, `v`, `w`.
Conversely `field2` is defined with two conflicting types `keyword` and `long`.
Note that `_field_caps` does not treat this case as an error but rather return the list of unique types seen for this field.
Jim Ferenczi 8 years ago
parent
commit
a8250b26e7
21 changed files with 1727 additions and 3 deletions
  1. 7 0
      core/src/main/java/org/elasticsearch/action/ActionModule.java
  2. 280 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java
  3. 44 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesAction.java
  4. 65 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexRequest.java
  5. 97 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java
  6. 143 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java
  7. 41 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestBuilder.java
  8. 106 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java
  9. 134 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java
  10. 121 0
      core/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesIndexAction.java
  11. 19 0
      core/src/main/java/org/elasticsearch/client/Client.java
  12. 3 1
      core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java
  13. 19 0
      core/src/main/java/org/elasticsearch/client/support/AbstractClient.java
  14. 2 2
      core/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java
  15. 88 0
      core/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java
  16. 53 0
      core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequestTests.java
  17. 60 0
      core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java
  18. 109 0
      core/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java
  19. 126 0
      docs/reference/search/field-caps.asciidoc
  20. 43 0
      rest-api-spec/src/main/resources/rest-api-spec/api/field_caps.json
  21. 167 0
      rest-api-spec/src/main/resources/rest-api-spec/test/field_caps/10_basic.yaml

+ 7 - 0
core/src/main/java/org/elasticsearch/action/ActionModule.java

@@ -149,6 +149,9 @@ import org.elasticsearch.action.delete.DeleteAction;
 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;
 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 class ActionModule extends AbstractModule {
         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 class ActionModule extends AbstractModule {
         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));

+ 280 - 0
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<IndexCaps> 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;
+        }
+    }
+}

+ 44 - 0
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<FieldCapabilitiesRequest,
+    FieldCapabilitiesResponse, FieldCapabilitiesRequestBuilder> {
+
+    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);
+    }
+}

+ 65 - 0
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<FieldCapabilitiesIndexRequest> {
+
+    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;
+    }
+}

+ 97 - 0
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<String, FieldCapabilities> responseMap;
+
+    FieldCapabilitiesIndexResponse(String indexName, Map<String, FieldCapabilities> responseMap) {
+        this.indexName = indexName;
+        this.responseMap = responseMap;
+    }
+
+    FieldCapabilitiesIndexResponse() {
+    }
+
+
+    /**
+     * Get the index name
+     */
+    public String getIndexName() {
+        return indexName;
+    }
+
+    /**
+     * Get the field capabilities map
+     */
+    public Map<String, FieldCapabilities> 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();
+    }
+}

+ 143 - 0
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<FieldCapabilitiesRequest, Void> 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<String> 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;
+    }
+}

+ 41 - 0
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<FieldCapabilitiesRequest, FieldCapabilitiesResponse,
+        FieldCapabilitiesRequestBuilder> {
+    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;
+    }
+}

+ 106 - 0
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<String, Map<String, FieldCapabilities>> responseMap;
+
+    FieldCapabilitiesResponse(Map<String, Map<String, FieldCapabilities>> responseMap) {
+        this.responseMap = responseMap;
+    }
+
+    /**
+     * Used for serialization
+     */
+    FieldCapabilitiesResponse() {
+        this.responseMap = Collections.emptyMap();
+    }
+
+    /**
+     * Get the field capabilities map.
+     */
+    public Map<String, Map<String, FieldCapabilities>> get() {
+        return responseMap;
+    }
+
+    /**
+     *
+     * Get the field capabilities per type for the provided {@code field}.
+     */
+    public Map<String, FieldCapabilities> 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<String, FieldCapabilities> 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<String, FieldCapabilities> 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();
+    }
+}

+ 134 - 0
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<FieldCapabilitiesRequest, FieldCapabilitiesResponse> {
+    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<FieldCapabilitiesResponse> 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<Object> 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<FieldCapabilitiesIndexResponse> () {
+                    @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<Object> indexResponses) {
+        Map<String, Map<String, FieldCapabilities.Builder>> 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<String, FieldCapabilities.Builder> 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<String, Map<String, FieldCapabilities>> responseMap = new HashMap<>();
+        for (Map.Entry<String, Map<String, FieldCapabilities.Builder>> entry :
+            responseMapBuilder.entrySet()) {
+            Map<String, FieldCapabilities> typeMap = new HashMap<>();
+            boolean multiTypes = entry.getValue().size() > 1;
+            for (Map.Entry<String, FieldCapabilities.Builder> fieldEntry :
+                entry.getValue().entrySet()) {
+                typeMap.put(fieldEntry.getKey(), fieldEntry.getValue().build(multiTypes));
+            }
+            responseMap.put(entry.getKey(), typeMap);
+        }
+
+        return new FieldCapabilitiesResponse(responseMap);
+    }
+}

+ 121 - 0
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<FieldCapabilitiesIndexRequest,
+    FieldCapabilitiesIndexResponse> {
+
+    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<String> fieldNames = new HashSet<>();
+        for (String field : request.fields()) {
+            fieldNames.addAll(mapperService.simpleMatchToIndexNames(field));
+        }
+        Map<String, FieldCapabilities> 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());
+    }
+}

+ 19 - 0
core/src/main/java/org/elasticsearch/client/Client.java

@@ -30,6 +30,10 @@ import org.elasticsearch.action.delete.DeleteResponse;
 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<FieldStatsResponse> listener);
 
+    /**
+     * Builder for the field capabilities request.
+     */
+    FieldCapabilitiesRequestBuilder prepareFieldCaps();
+
+    /**
+     * An action that returns the field capabilities from the provided request
+     */
+    ActionFuture<FieldCapabilitiesResponse> fieldCaps(FieldCapabilitiesRequest request);
+
+    /**
+     * An action that returns the field capabilities from the provided request
+     */
+    void fieldCaps(FieldCapabilitiesRequest request, ActionListener<FieldCapabilitiesResponse> listener);
+
     /**
      * Returns this clients settings
      */

+ 3 - 1
core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java

@@ -50,6 +50,9 @@ import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRespon
 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<RolloverResponse> listener);
-
 }

+ 19 - 0
core/src/main/java/org/elasticsearch/client/support/AbstractClient.java

@@ -272,6 +272,10 @@ import org.elasticsearch.action.explain.ExplainAction;
 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 abstract class AbstractClient extends AbstractComponent implements Client
         return new FieldStatsRequestBuilder(this, FieldStatsAction.INSTANCE);
     }
 
+    @Override
+    public void fieldCaps(FieldCapabilitiesRequest request, ActionListener<FieldCapabilitiesResponse> listener) {
+        execute(FieldCapabilitiesAction.INSTANCE, request, listener);
+    }
+
+    @Override
+    public ActionFuture<FieldCapabilitiesResponse> 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;

+ 2 - 2
core/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java

@@ -313,14 +313,14 @@ public abstract class MappedFieldType extends FieldType {
     /** 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;

+ 88 - 0
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<FieldCapabilitiesResponse>(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);
+            }
+        });
+    }
+}

+ 53 - 0
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());
+        }
+    }
+}

+ 60 - 0
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<String, Map<String, FieldCapabilities> > 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<String, FieldCapabilities> 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());
+        }
+    }
+}

+ 109 - 0
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<FieldCapabilities> {
+    @Override
+    protected FieldCapabilities createTestInstance() {
+        return randomFieldCaps();
+    }
+
+    @Override
+    protected Writeable.Reader<FieldCapabilities> 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);
+    }
+}

+ 126 - 0
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.
+
+
+
+
+
+
+

+ 43 - 0
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
+    }
+  }
+}

+ 167 - 0
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