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.
This commit is contained in:
Jim Ferenczi 2017-03-31 15:34:46 +02:00 committed by GitHub
parent 5eba90f37c
commit a8250b26e7
21 changed files with 1727 additions and 3 deletions

View File

@ -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));

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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
*/

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
});
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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
}
}
}

View File

@ -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