Kaynağa Gözat

Search Exists API: Checks if any matching documents exist for a given query

Implements a new Exists API allowing users to do fast exists check on any matched documents for a given query.
This API should be faster then using the Count API as it will:
 - early terminate the search execution once any document is found to exist
 - return the response as soon as the first shard reports matched documents

closes #6995
Areek Zillur 11 yıl önce
ebeveyn
işleme
1d581e6286
21 değiştirilmiş dosya ile 1364 ekleme ve 5 silme
  1. 2 0
      docs/reference/search.asciidoc
  2. 86 0
      docs/reference/search/exists.asciidoc
  3. 55 0
      rest-api-spec/api/search_exists.json
  4. 3 0
      src/main/java/org/elasticsearch/action/ActionModule.java
  5. 43 0
      src/main/java/org/elasticsearch/action/exists/ExistsAction.java
  6. 249 0
      src/main/java/org/elasticsearch/action/exists/ExistsRequest.java
  7. 130 0
      src/main/java/org/elasticsearch/action/exists/ExistsRequestBuilder.java
  8. 61 0
      src/main/java/org/elasticsearch/action/exists/ExistsResponse.java
  9. 121 0
      src/main/java/org/elasticsearch/action/exists/ShardExistsRequest.java
  10. 55 0
      src/main/java/org/elasticsearch/action/exists/ShardExistsResponse.java
  11. 244 0
      src/main/java/org/elasticsearch/action/exists/TransportExistsAction.java
  12. 23 0
      src/main/java/org/elasticsearch/action/exists/package-info.java
  13. 5 5
      src/main/java/org/elasticsearch/action/support/broadcast/TransportBroadcastOperationAction.java
  14. 26 0
      src/main/java/org/elasticsearch/client/Client.java
  15. 13 0
      src/main/java/org/elasticsearch/client/Requests.java
  16. 19 0
      src/main/java/org/elasticsearch/client/support/AbstractClient.java
  17. 1 0
      src/main/java/org/elasticsearch/rest/action/RestActionModule.java
  18. 81 0
      src/main/java/org/elasticsearch/rest/action/exists/RestExistsAction.java
  19. 9 0
      src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java
  20. 131 0
      src/test/java/org/elasticsearch/exists/SimpleExistsTests.java
  21. 7 0
      src/test/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java

+ 2 - 0
docs/reference/search.asciidoc

@@ -95,6 +95,8 @@ include::search/multi-search.asciidoc[]
 
 include::search/count.asciidoc[]
 
+include::search/exists.asciidoc[]
+
 include::search/validate.asciidoc[]
 
 include::search/explain.asciidoc[]

+ 86 - 0
docs/reference/search/exists.asciidoc

@@ -0,0 +1,86 @@
+[[search-exists]]
+== Search Exists API
+
+The exists API allows to easily determine if any
+matching documents exist for a provided query. It can be executed across one or more indices
+and across one or more types. The query can either be provided using a
+simple query string as a parameter, or using the
+<<query-dsl,Query DSL>> defined within the request
+body. Here is an example:
+
+[source,js]
+--------------------------------------------------
+$ curl -XGET 'http://localhost:9200/twitter/tweet/_search/exists?q=user:kimchy'
+
+$ curl -XGET 'http://localhost:9200/twitter/tweet/_search/exists' -d '
+{
+    "query" : {
+        "term" : { "user" : "kimchy" }
+    }
+}'
+
+--------------------------------------------------
+
+NOTE: The query being sent in the body must be nested in a `query` key, same as
+how the <<search-search,search api>> works.
+
+Both the examples above do the same thing, which is determine the existence of
+tweets from the twitter index for a certain user. The response body will be of
+the following format:
+
+[source,js]
+--------------------------------------------------
+{
+    "exists" : true
+}
+--------------------------------------------------
+
+[float]
+=== Multi index, Multi type
+
+The exists API can be applied to <<search-multi-index-type,multiple types in multiple indices>>.
+
+[float]
+=== Request Parameters
+
+When executing exists using the query parameter `q`, the query passed is
+a query string using Lucene query parser. There are additional
+parameters that can be passed:
+
+[cols="<,<",options="header",]
+|=======================================================================
+|Name |Description
+|df |The default field to use when no field prefix is defined within the
+query.
+
+|analyzer |The analyzer name to be used when analyzing the query string.
+
+|default_operator |The default operator to be used, can be `AND` or
+`OR`. Defaults to `OR`.
+
+|=======================================================================
+
+[float]
+=== Request Body
+
+The exists API can use the <<query-dsl,Query DSL>> within
+its body in order to express the query that should be executed. The body
+content can also be passed as a REST parameter named `source`.
+
+HTTP GET and HTTP POST can be used to execute exists with body.
+Since not all clients support GET with body, POST is allowed as well.
+
+[float]
+=== Distributed
+
+The exists operation is broadcast across all shards. For each shard id
+group, a replica is chosen and executed against it. This means that
+replicas increase the scalability of exists. The exists operation also
+early terminates shard requests once the first shard reports matched
+document existence.
+
+[float]
+=== Routing
+
+The routing value (a comma separated list of the routing values) can be
+specified to control which shards the exists request will be executed on.

+ 55 - 0
rest-api-spec/api/search_exists.json

@@ -0,0 +1,55 @@
+{
+  "search_exists": {
+    "documentation": "http://www.elasticsearch.org/guide/en/elasticsearch/reference/master/exists.html",
+    "methods": ["POST", "GET"],
+    "url": {
+      "path": "/_search/exists",
+      "paths": ["/_search/exists", "/{index}/_search/exists", "/{index}/{type}/_search/exists"],
+      "parts": {
+        "index": {
+          "type" : "list",
+          "description" : "A comma-separated list of indices to restrict the results"
+        },
+        "type": {
+          "type" : "list",
+          "description" : "A comma-separated list of types to restrict the results"
+        }
+      },
+      "params": {
+        "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"],
+            "default" : "open",
+            "description" : "Whether to expand wildcard expression to concrete indices that are open, closed or both."
+        },
+        "min_score": {
+          "type" : "number",
+          "description" : "Include only documents with a specific `_score` value in the result"
+        },
+        "preference": {
+          "type" : "string",
+          "description" : "Specify the node or shard the operation should be performed on (default: random)"
+        },
+        "routing": {
+          "type" : "string",
+          "description" : "Specific routing value"
+        },
+        "source": {
+          "type" : "string",
+          "description" : "The URL-encoded query definition (instead of using the request body)"
+        }
+      }
+    },
+    "body": {
+      "description" : "A query to restrict the results specified with the Query DSL (optional)"
+    }
+  }
+}

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

@@ -132,6 +132,8 @@ import org.elasticsearch.action.deletebyquery.DeleteByQueryAction;
 import org.elasticsearch.action.deletebyquery.TransportDeleteByQueryAction;
 import org.elasticsearch.action.deletebyquery.TransportIndexDeleteByQueryAction;
 import org.elasticsearch.action.deletebyquery.TransportShardDeleteByQueryAction;
+import org.elasticsearch.action.exists.ExistsAction;
+import org.elasticsearch.action.exists.TransportExistsAction;
 import org.elasticsearch.action.explain.ExplainAction;
 import org.elasticsearch.action.explain.TransportExplainAction;
 import org.elasticsearch.action.get.*;
@@ -278,6 +280,7 @@ public class ActionModule extends AbstractModule {
         registerAction(DeleteAction.INSTANCE, TransportDeleteAction.class,
                 TransportIndexDeleteAction.class, TransportShardDeleteAction.class);
         registerAction(CountAction.INSTANCE, TransportCountAction.class);
+        registerAction(ExistsAction.INSTANCE, TransportExistsAction.class);
         registerAction(SuggestAction.INSTANCE, TransportSuggestAction.class);
         registerAction(UpdateAction.INSTANCE, TransportUpdateAction.class);
         registerAction(MultiGetAction.INSTANCE, TransportMultiGetAction.class,

+ 43 - 0
src/main/java/org/elasticsearch/action/exists/ExistsAction.java

@@ -0,0 +1,43 @@
+/*
+ * 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.exists;
+
+
+import org.elasticsearch.action.ClientAction;
+import org.elasticsearch.client.Client;
+
+public class ExistsAction extends ClientAction<ExistsRequest, ExistsResponse, ExistsRequestBuilder> {
+
+    public static final ExistsAction INSTANCE = new ExistsAction();
+    public static final String NAME = "exists";
+
+    private ExistsAction() {
+        super(NAME);
+    }
+
+    @Override
+    public ExistsResponse newResponse() {
+        return new ExistsResponse();
+    }
+
+    @Override
+    public ExistsRequestBuilder newRequestBuilder(Client client) {
+        return new ExistsRequestBuilder(client);
+    }
+}

+ 249 - 0
src/main/java/org/elasticsearch/action/exists/ExistsRequest.java

@@ -0,0 +1,249 @@
+/*
+ * 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.exists;
+
+import org.elasticsearch.ElasticsearchGenerationException;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.QuerySourceBuilder;
+import org.elasticsearch.action.support.broadcast.BroadcastOperationRequest;
+import org.elasticsearch.client.Requests;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentHelper;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+public class ExistsRequest extends BroadcastOperationRequest<ExistsRequest> {
+
+    public static final float DEFAULT_MIN_SCORE = -1f;
+    private float minScore = DEFAULT_MIN_SCORE;
+
+    @Nullable
+    protected String routing;
+
+    @Nullable
+    private String preference;
+
+    private BytesReference source;
+    private boolean sourceUnsafe;
+
+    private String[] types = Strings.EMPTY_ARRAY;
+
+    long nowInMillis;
+
+    ExistsRequest() {
+    }
+
+    /**
+     * Constructs a new exists request against the provided indices. No indices provided means it will
+     * run against all indices.
+     */
+    public ExistsRequest(String... indices) {
+        super(indices);
+    }
+
+
+    @Override
+    public ActionRequestValidationException validate() {
+        ActionRequestValidationException validationException = super.validate();
+        return validationException;
+    }
+
+    @Override
+    protected void beforeStart() {
+        if (sourceUnsafe) {
+            source = source.copyBytesArray();
+            sourceUnsafe = false;
+        }
+    }
+
+    /**
+     * The minimum score of the documents to include in the count.
+     */
+    float minScore() {
+        return minScore;
+    }
+
+    /**
+     * The minimum score of the documents to include in the count. Defaults to <tt>-1</tt> which means all
+     * documents will be considered.
+     */
+    public ExistsRequest minScore(float minScore) {
+        this.minScore = minScore;
+        return this;
+    }
+
+    /**
+     * A comma separated list of routing values to control the shards the search will be executed on.
+     */
+    public String routing() {
+        return this.routing;
+    }
+
+    /**
+     * A comma separated list of routing values to control the shards the search will be executed on.
+     */
+    public ExistsRequest routing(String routing) {
+        this.routing = routing;
+        return this;
+    }
+
+    /**
+     * The routing values to control the shards that the search will be executed on.
+     */
+    public ExistsRequest routing(String... routings) {
+        this.routing = Strings.arrayToCommaDelimitedString(routings);
+        return this;
+    }
+
+    /**
+     * Routing preference for executing the search on shards
+     */
+    public ExistsRequest preference(String preference) {
+        this.preference = preference;
+        return this;
+    }
+
+    public String preference() {
+        return this.preference;
+    }
+
+    /**
+     * The source to execute.
+     */
+    BytesReference source() {
+        return source;
+    }
+
+    /**
+     * The source to execute.
+     */
+    public ExistsRequest source(QuerySourceBuilder sourceBuilder) {
+        this.source = sourceBuilder.buildAsBytes(Requests.CONTENT_TYPE);
+        this.sourceUnsafe = false;
+        return this;
+    }
+
+    /**
+     * The source to execute in the form of a map.
+     */
+    public ExistsRequest source(Map querySource) {
+        try {
+            XContentBuilder builder = XContentFactory.contentBuilder(Requests.CONTENT_TYPE);
+            builder.map(querySource);
+            return source(builder);
+        } catch (IOException e) {
+            throw new ElasticsearchGenerationException("Failed to generate [" + querySource + "]", e);
+        }
+    }
+
+    public ExistsRequest source(XContentBuilder builder) {
+        this.source = builder.bytes();
+        this.sourceUnsafe = false;
+        return this;
+    }
+
+    /**
+     * The source to execute. It is preferable to use either {@link #source(byte[])}
+     * or {@link #source(QuerySourceBuilder)}.
+     */
+    public ExistsRequest source(String querySource) {
+        this.source = new BytesArray(querySource);
+        this.sourceUnsafe = false;
+        return this;
+    }
+
+    /**
+     * The source to execute.
+     */
+    public ExistsRequest source(byte[] querySource) {
+        return source(querySource, 0, querySource.length, false);
+    }
+
+    /**
+     * The source to execute.
+     */
+    public ExistsRequest source(byte[] querySource, int offset, int length, boolean unsafe) {
+        return source(new BytesArray(querySource, offset, length), unsafe);
+    }
+
+    public ExistsRequest source(BytesReference querySource, boolean unsafe) {
+        this.source = querySource;
+        this.sourceUnsafe = unsafe;
+        return this;
+    }
+
+    /**
+     * The types of documents the query will run against. Defaults to all types.
+     */
+    public String[] types() {
+        return this.types;
+    }
+
+    /**
+     * The types of documents the query will run against. Defaults to all types.
+     */
+    public ExistsRequest types(String... types) {
+        this.types = types;
+        return this;
+    }
+
+    @Override
+    public void readFrom(StreamInput in) throws IOException {
+        super.readFrom(in);
+        minScore = in.readFloat();
+        routing = in.readOptionalString();
+        preference = in.readOptionalString();
+        sourceUnsafe = false;
+        source = in.readBytesReference();
+        types = in.readStringArray();
+
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        out.writeFloat(minScore);
+        out.writeOptionalString(routing);
+        out.writeOptionalString(preference);
+        out.writeBytesReference(source);
+        out.writeStringArray(types);
+
+    }
+
+    @Override
+    public String toString() {
+        String sSource = "_na_";
+        try {
+            sSource = XContentHelper.convertToJson(source, false);
+        } catch (Exception e) {
+            // ignore
+        }
+        return "[" + Arrays.toString(indices) + "]" + Arrays.toString(types) + ", source[" + sSource + "]";
+    }
+}

+ 130 - 0
src/main/java/org/elasticsearch/action/exists/ExistsRequestBuilder.java

@@ -0,0 +1,130 @@
+/*
+ * 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.exists;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.QuerySourceBuilder;
+import org.elasticsearch.action.support.broadcast.BroadcastOperationRequestBuilder;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.index.query.QueryBuilder;
+
+public class ExistsRequestBuilder extends BroadcastOperationRequestBuilder<ExistsRequest, ExistsResponse, ExistsRequestBuilder, Client> {
+
+
+    private QuerySourceBuilder sourceBuilder;
+
+    public ExistsRequestBuilder(Client client) {
+        super(client, new ExistsRequest());
+    }
+
+    /**
+     * The types of documents the query will run against. Defaults to all types.
+     */
+    public ExistsRequestBuilder setTypes(String... types) {
+        request.types(types);
+        return this;
+    }
+
+    /**
+     * A comma separated list of routing values to control the shards the search will be executed on.
+     */
+    public ExistsRequestBuilder setRouting(String routing) {
+        request.routing(routing);
+        return this;
+    }
+
+    /**
+     * Sets the preference to execute the search. Defaults to randomize across shards. Can be set to
+     * <tt>_local</tt> to prefer local shards, <tt>_primary</tt> to execute only on primary shards,
+     * _shards:x,y to operate on shards x & y, or a custom value, which guarantees that the same order
+     * will be used across different requests.
+     */
+    public ExistsRequestBuilder setPreference(String preference) {
+        request.preference(preference);
+        return this;
+    }
+
+    /**
+     * The routing values to control the shards that the search will be executed on.
+     */
+    public ExistsRequestBuilder setRouting(String... routing) {
+        request.routing(routing);
+        return this;
+    }
+
+    /**
+     * The query source to execute.
+     *
+     * @see org.elasticsearch.index.query.QueryBuilders
+     */
+    public ExistsRequestBuilder setQuery(QueryBuilder queryBuilder) {
+        sourceBuilder().setQuery(queryBuilder);
+        return this;
+    }
+
+    /**
+     * The query binary to execute
+     */
+    public ExistsRequestBuilder setQuery(BytesReference queryBinary) {
+        sourceBuilder().setQuery(queryBinary);
+        return this;
+    }
+
+    /**
+     * The source to execute.
+     */
+    public ExistsRequestBuilder setSource(BytesReference source) {
+        request().source(source, false);
+        return this;
+    }
+
+    /**
+     * The source to execute.
+     */
+    public ExistsRequestBuilder setSource(BytesReference source, boolean unsafe) {
+        request().source(source, unsafe);
+        return this;
+    }
+
+    /**
+     * The query source to execute.
+     */
+    public ExistsRequestBuilder setSource(byte[] querySource) {
+        request.source(querySource);
+        return this;
+    }
+
+    @Override
+    protected void doExecute(ActionListener<ExistsResponse> listener) {
+        if (sourceBuilder != null) {
+            request.source(sourceBuilder);
+        }
+
+        client.exists(request, listener);
+    }
+
+    private QuerySourceBuilder sourceBuilder() {
+        if (sourceBuilder == null) {
+            sourceBuilder = new QuerySourceBuilder();
+        }
+        return sourceBuilder;
+    }
+
+}

+ 61 - 0
src/main/java/org/elasticsearch/action/exists/ExistsResponse.java

@@ -0,0 +1,61 @@
+/*
+ * 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.exists;
+
+import org.elasticsearch.action.ShardOperationFailedException;
+import org.elasticsearch.action.support.broadcast.BroadcastOperationResponse;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+import java.util.List;
+
+public class ExistsResponse extends BroadcastOperationResponse {
+
+    private boolean exists = false;
+
+    ExistsResponse() {
+
+    }
+
+    ExistsResponse(boolean exists, int totalShards, int successfulShards, int failedShards, List<ShardOperationFailedException> shardFailures) {
+        super(totalShards, successfulShards, failedShards, shardFailures);
+        this.exists = exists;
+    }
+
+    /**
+     * Whether the documents matching the query provided exists
+     */
+    public boolean exists() {
+        return exists;
+    }
+
+    @Override
+    public void readFrom(StreamInput in) throws IOException {
+        super.readFrom(in);
+        exists = in.readBoolean();
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        out.writeBoolean(exists);
+    }
+}

+ 121 - 0
src/main/java/org/elasticsearch/action/exists/ShardExistsRequest.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.exists;
+
+import org.elasticsearch.action.support.broadcast.BroadcastShardOperationRequest;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+
+public class ShardExistsRequest extends BroadcastShardOperationRequest {
+
+    private float minScore;
+
+    private BytesReference querySource;
+
+    private String[] types = Strings.EMPTY_ARRAY;
+
+    private long nowInMillis;
+
+    @Nullable
+    private String[] filteringAliases;
+
+    ShardExistsRequest() {
+    }
+
+    public ShardExistsRequest(String index, int shardId, @Nullable String[] filteringAliases, ExistsRequest request) {
+        super(index, shardId, request);
+        this.minScore = request.minScore();
+        this.querySource = request.source();
+        this.types = request.types();
+        this.filteringAliases = filteringAliases;
+        this.nowInMillis = request.nowInMillis;
+    }
+
+    public float minScore() {
+        return minScore;
+    }
+
+    public BytesReference querySource() {
+        return querySource;
+    }
+
+    public String[] types() {
+        return this.types;
+    }
+
+    public String[] filteringAliases() {
+        return filteringAliases;
+    }
+
+    public long nowInMillis() {
+        return this.nowInMillis;
+    }
+
+    @Override
+    public void readFrom(StreamInput in) throws IOException {
+        super.readFrom(in);
+        minScore = in.readFloat();
+
+        querySource = in.readBytesReference();
+
+        int typesSize = in.readVInt();
+        if (typesSize > 0) {
+            types = new String[typesSize];
+            for (int i = 0; i < typesSize; i++) {
+                types[i] = in.readString();
+            }
+        }
+        int aliasesSize = in.readVInt();
+        if (aliasesSize > 0) {
+            filteringAliases = new String[aliasesSize];
+            for (int i = 0; i < aliasesSize; i++) {
+                filteringAliases[i] = in.readString();
+            }
+        }
+        nowInMillis = in.readVLong();
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        out.writeFloat(minScore);
+
+        out.writeBytesReference(querySource);
+
+        out.writeVInt(types.length);
+        for (String type : types) {
+            out.writeString(type);
+        }
+        if (filteringAliases != null) {
+            out.writeVInt(filteringAliases.length);
+            for (String alias : filteringAliases) {
+                out.writeString(alias);
+            }
+        } else {
+            out.writeVInt(0);
+        }
+        out.writeVLong(nowInMillis);
+    }
+}

+ 55 - 0
src/main/java/org/elasticsearch/action/exists/ShardExistsResponse.java

@@ -0,0 +1,55 @@
+/*
+ * 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.exists;
+
+import org.elasticsearch.action.support.broadcast.BroadcastShardOperationResponse;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+
+public class ShardExistsResponse extends BroadcastShardOperationResponse {
+
+    private boolean exists;
+
+    ShardExistsResponse() {
+    }
+
+    public ShardExistsResponse(String index, int shardId, boolean exists) {
+        super(index, shardId);
+        this.exists = exists;
+    }
+
+    public boolean exists() {
+        return this.exists;
+    }
+
+    @Override
+    public void readFrom(StreamInput in) throws IOException {
+        super.readFrom(in);
+        exists = in.readBoolean();
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        out.writeBoolean(exists);
+    }
+}

+ 244 - 0
src/main/java/org/elasticsearch/action/exists/TransportExistsAction.java

@@ -0,0 +1,244 @@
+/*
+ * 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.exists;
+
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ShardOperationFailedException;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.DefaultShardOperationFailedException;
+import org.elasticsearch.action.support.broadcast.BroadcastShardOperationFailedException;
+import org.elasticsearch.action.support.broadcast.TransportBroadcastOperationAction;
+import org.elasticsearch.cache.recycler.CacheRecycler;
+import org.elasticsearch.cache.recycler.PageCacheRecycler;
+import org.elasticsearch.cluster.ClusterService;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.block.ClusterBlockException;
+import org.elasticsearch.cluster.block.ClusterBlockLevel;
+import org.elasticsearch.cluster.routing.GroupShardsIterator;
+import org.elasticsearch.cluster.routing.ShardIterator;
+import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.lucene.Lucene;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.BigArrays;
+import org.elasticsearch.index.query.QueryParseContext;
+import org.elasticsearch.index.service.IndexService;
+import org.elasticsearch.index.shard.service.IndexShard;
+import org.elasticsearch.indices.IndicesService;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.search.SearchShardTarget;
+import org.elasticsearch.search.internal.DefaultSearchContext;
+import org.elasticsearch.search.internal.SearchContext;
+import org.elasticsearch.search.internal.ShardSearchRequest;
+import org.elasticsearch.search.query.QueryPhaseExecutionException;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.elasticsearch.action.exists.ExistsRequest.DEFAULT_MIN_SCORE;
+
+public class TransportExistsAction extends TransportBroadcastOperationAction<ExistsRequest, ExistsResponse, ShardExistsRequest, ShardExistsResponse> {
+
+    private final IndicesService indicesService;
+
+    private final ScriptService scriptService;
+
+    private final CacheRecycler cacheRecycler;
+
+    private final PageCacheRecycler pageCacheRecycler;
+
+    private final BigArrays bigArrays;
+
+    @Inject
+    public TransportExistsAction(Settings settings, ThreadPool threadPool, ClusterService clusterService, TransportService transportService,
+                                IndicesService indicesService, ScriptService scriptService, CacheRecycler cacheRecycler,
+                                PageCacheRecycler pageCacheRecycler, BigArrays bigArrays, ActionFilters actionFilters) {
+        super(settings, ExistsAction.NAME, threadPool, clusterService, transportService, actionFilters);
+        this.indicesService = indicesService;
+        this.scriptService = scriptService;
+        this.cacheRecycler = cacheRecycler;
+        this.pageCacheRecycler = pageCacheRecycler;
+        this.bigArrays = bigArrays;
+    }
+
+    @Override
+    protected void doExecute(ExistsRequest request, ActionListener<ExistsResponse> listener) {
+        request.nowInMillis = System.currentTimeMillis();
+        new ExistsAsyncBroadcastAction(request, listener).start();
+    }
+
+    @Override
+    protected String executor() {
+        return ThreadPool.Names.SEARCH;
+    }
+
+    @Override
+    protected ExistsRequest newRequest() {
+        return new ExistsRequest();
+    }
+
+    @Override
+    protected ShardExistsRequest newShardRequest() {
+        return new ShardExistsRequest();
+    }
+
+    @Override
+    protected ShardExistsRequest newShardRequest(int numShards, ShardRouting shard, ExistsRequest request) {
+        String[] filteringAliases = clusterService.state().metaData().filteringAliases(shard.index(), request.indices());
+        return new ShardExistsRequest(shard.index(), shard.id(), filteringAliases, request);
+    }
+
+    @Override
+    protected ShardExistsResponse newShardResponse() {
+        return new ShardExistsResponse();
+    }
+
+    @Override
+    protected GroupShardsIterator shards(ClusterState clusterState, ExistsRequest request, String[] concreteIndices) {
+        Map<String, Set<String>> routingMap = clusterState.metaData().resolveSearchRouting(request.routing(), request.indices());
+        return clusterService.operationRouting().searchShards(clusterState, request.indices(), concreteIndices, routingMap, request.preference());
+    }
+
+    @Override
+    protected ClusterBlockException checkGlobalBlock(ClusterState state, ExistsRequest request) {
+        return state.blocks().globalBlockedException(ClusterBlockLevel.READ);
+    }
+
+    @Override
+    protected ClusterBlockException checkRequestBlock(ClusterState state, ExistsRequest countRequest, String[] concreteIndices) {
+        return state.blocks().indicesBlockedException(ClusterBlockLevel.READ, concreteIndices);
+    }
+
+    @Override
+    protected ExistsResponse newResponse(ExistsRequest request, AtomicReferenceArray shardsResponses, ClusterState clusterState) {
+        int successfulShards = 0;
+        int failedShards = 0;
+        boolean exists = false;
+        List<ShardOperationFailedException> shardFailures = null;
+
+        // if docs do exist, the last response will have exists = true (since we early terminate the shard requests)
+        for (int i = shardsResponses.length() - 1; i >= 0 ; i--) {
+            Object shardResponse = shardsResponses.get(i);
+            if (shardResponse == null) {
+                // simply ignore non active shards
+            } else if (shardResponse instanceof BroadcastShardOperationFailedException) {
+                failedShards++;
+                if (shardFailures == null) {
+                    shardFailures = newArrayList();
+                }
+                shardFailures.add(new DefaultShardOperationFailedException((BroadcastShardOperationFailedException) shardResponse));
+            } else {
+                successfulShards++;
+                if ((exists = ((ShardExistsResponse) shardResponse).exists())) {
+                    successfulShards = shardsResponses.length() - failedShards;
+                    break;
+                }
+            }
+        }
+        return new ExistsResponse(exists, shardsResponses.length(), successfulShards, failedShards, shardFailures);
+    }
+
+    @Override
+    protected ShardExistsResponse shardOperation(ShardExistsRequest request) throws ElasticsearchException {
+        IndexService indexService = indicesService.indexServiceSafe(request.index());
+        IndexShard indexShard = indexService.shardSafe(request.shardId());
+
+        SearchShardTarget shardTarget = new SearchShardTarget(clusterService.localNode().id(), request.index(), request.shardId());
+        SearchContext context = new DefaultSearchContext(0,
+                new ShardSearchRequest().types(request.types())
+                        .filteringAliases(request.filteringAliases())
+                        .nowInMillis(request.nowInMillis()),
+                shardTarget, indexShard.acquireSearcher("exists"), indexService, indexShard,
+                scriptService, cacheRecycler, pageCacheRecycler, bigArrays);
+        SearchContext.setCurrent(context);
+
+        try {
+            if (request.minScore() != DEFAULT_MIN_SCORE) {
+                context.minimumScore(request.minScore());
+            }
+            BytesReference source = request.querySource();
+            if (source != null && source.length() > 0) {
+                try {
+                    QueryParseContext.setTypes(request.types());
+                    context.parsedQuery(indexService.queryParserService().parseQuery(source));
+                } finally {
+                    QueryParseContext.removeTypes();
+                }
+            }
+            context.preProcess();
+            try {
+                Lucene.EarlyTerminatingCollector existsCollector = Lucene.createExistsCollector();
+                Lucene.exists(context.searcher(), context.query(), existsCollector);
+                return new ShardExistsResponse(request.index(), request.shardId(), existsCollector.exists());
+            } catch (Exception e) {
+                throw new QueryPhaseExecutionException(context, "failed to execute exists", e);
+            }
+        } finally {
+            // this will also release the index searcher
+            context.close();
+            SearchContext.removeCurrent();
+        }
+    }
+
+    /**
+     * An async broadcast action that early terminates shard request
+     * upon any shard response reporting matched doc existence
+     */
+    final private class ExistsAsyncBroadcastAction extends AsyncBroadcastAction  {
+
+        final AtomicBoolean processed = new AtomicBoolean(false);
+
+        ExistsAsyncBroadcastAction(ExistsRequest request, ActionListener<ExistsResponse> listener) {
+            super(request, listener);
+        }
+
+        @Override
+        protected void onOperation(ShardRouting shard, int shardIndex, ShardExistsResponse response) {
+            super.onOperation(shard, shardIndex, response);
+            if (response.exists()) {
+                finishHim();
+            }
+        }
+
+        @Override
+        protected void performOperation(final ShardIterator shardIt, final ShardRouting shard, final int shardIndex) {
+            if (processed.get()) {
+                return;
+            }
+            super.performOperation(shardIt, shard, shardIndex);
+        }
+
+        @Override
+        protected void finishHim() {
+            if (processed.compareAndSet(false, true)) {
+                super.finishHim();
+            }
+        }
+    }
+}

+ 23 - 0
src/main/java/org/elasticsearch/action/exists/package-info.java

@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Exists action.
+ */
+package org.elasticsearch.action.exists;

+ 5 - 5
src/main/java/org/elasticsearch/action/support/broadcast/TransportBroadcastOperationAction.java

@@ -95,7 +95,7 @@ public abstract class TransportBroadcastOperationAction<Request extends Broadcas
 
     protected abstract ClusterBlockException checkRequestBlock(ClusterState state, Request request, String[] concreteIndices);
 
-    class AsyncBroadcastAction {
+    protected class AsyncBroadcastAction {
 
         private final Request request;
         private final ActionListener<Response> listener;
@@ -106,7 +106,7 @@ public abstract class TransportBroadcastOperationAction<Request extends Broadcas
         private final AtomicInteger counterOps = new AtomicInteger();
         private final AtomicReferenceArray shardsResponses;
 
-        AsyncBroadcastAction(Request request, ActionListener<Response> listener) {
+        protected AsyncBroadcastAction(Request request, ActionListener<Response> listener) {
             this.request = request;
             this.listener = listener;
 
@@ -156,7 +156,7 @@ public abstract class TransportBroadcastOperationAction<Request extends Broadcas
             }
         }
 
-        void performOperation(final ShardIterator shardIt, final ShardRouting shard, final int shardIndex) {
+        protected void performOperation(final ShardIterator shardIt, final ShardRouting shard, final int shardIndex) {
             if (shard == null) {
                 // no more active shards... (we should not really get here, just safety)
                 onOperation(null, shardIt, shardIndex, new NoShardAvailableActionException(shardIt.shardId()));
@@ -210,7 +210,7 @@ public abstract class TransportBroadcastOperationAction<Request extends Broadcas
         }
 
         @SuppressWarnings({"unchecked"})
-        void onOperation(ShardRouting shard, int shardIndex, ShardResponse response) {
+        protected void onOperation(ShardRouting shard, int shardIndex, ShardResponse response) {
             shardsResponses.set(shardIndex, response);
             if (expectedOps == counterOps.incrementAndGet()) {
                 finishHim();
@@ -247,7 +247,7 @@ public abstract class TransportBroadcastOperationAction<Request extends Broadcas
             }
         }
 
-        void finishHim() {
+        protected void finishHim() {
             try {
                 listener.onResponse(newResponse(request, shardsResponses, clusterState));
             } catch (Throwable e) {

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

@@ -33,6 +33,9 @@ import org.elasticsearch.action.delete.DeleteResponse;
 import org.elasticsearch.action.deletebyquery.DeleteByQueryRequest;
 import org.elasticsearch.action.deletebyquery.DeleteByQueryRequestBuilder;
 import org.elasticsearch.action.deletebyquery.DeleteByQueryResponse;
+import org.elasticsearch.action.exists.ExistsRequest;
+import org.elasticsearch.action.exists.ExistsRequestBuilder;
+import org.elasticsearch.action.exists.ExistsResponse;
 import org.elasticsearch.action.explain.ExplainRequest;
 import org.elasticsearch.action.explain.ExplainRequestBuilder;
 import org.elasticsearch.action.explain.ExplainResponse;
@@ -398,6 +401,29 @@ public interface Client extends ElasticsearchClient<Client>, Releasable {
      */
     CountRequestBuilder prepareCount(String... indices);
 
+    /**
+     * Checks existence of any documents matching a specific query.
+     *
+     * @param request The exists request
+     * @return The result future
+     * @see Requests#existsRequest(String...)
+     */
+    ActionFuture<ExistsResponse> exists(ExistsRequest request);
+
+    /**
+     * Checks existence of any documents matching a specific query.
+     *
+     * @param request The exists request
+     * @param listener A listener to be notified of the result
+     * @see Requests#existsRequest(String...)
+     */
+    void exists(ExistsRequest request, ActionListener<ExistsResponse> listener);
+
+    /**
+     * Checks existence of any documents matching a specific query.
+     */
+    ExistsRequestBuilder prepareExists(String... indices);
+
     /**
      * Suggestion matching a specific phrase.
      *

+ 13 - 0
src/main/java/org/elasticsearch/client/Requests.java

@@ -55,6 +55,7 @@ import org.elasticsearch.action.bulk.BulkRequest;
 import org.elasticsearch.action.count.CountRequest;
 import org.elasticsearch.action.delete.DeleteRequest;
 import org.elasticsearch.action.deletebyquery.DeleteByQueryRequest;
+import org.elasticsearch.action.exists.ExistsRequest;
 import org.elasticsearch.action.get.GetRequest;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.mlt.MoreLikeThisRequest;
@@ -148,6 +149,18 @@ public class Requests {
         return new CountRequest(indices);
     }
 
+    /**
+     * Creates a exists request which checks if any of the hits matched against a query exists. Note, the query itself must be set
+     * either using the JSON source of the query, or using a {@link org.elasticsearch.index.query.QueryBuilder} (using {@link org.elasticsearch.index.query.QueryBuilders}).
+     *
+     * @param indices The indices to count matched documents against a query. Use <tt>null</tt> or <tt>_all</tt> to execute against all indices
+     * @return The exists request
+     * @see org.elasticsearch.client.Client#exists(org.elasticsearch.action.exists.ExistsRequest)
+     */
+    public static ExistsRequest existsRequest(String... indices) {
+        return new ExistsRequest(indices);
+    }
+
     /**
      * More like this request represents a request to search for documents that are "like" the provided (fetched)
      * document.

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

@@ -38,6 +38,10 @@ import org.elasticsearch.action.deletebyquery.DeleteByQueryAction;
 import org.elasticsearch.action.deletebyquery.DeleteByQueryRequest;
 import org.elasticsearch.action.deletebyquery.DeleteByQueryRequestBuilder;
 import org.elasticsearch.action.deletebyquery.DeleteByQueryResponse;
+import org.elasticsearch.action.exists.ExistsAction;
+import org.elasticsearch.action.exists.ExistsRequest;
+import org.elasticsearch.action.exists.ExistsRequestBuilder;
+import org.elasticsearch.action.exists.ExistsResponse;
 import org.elasticsearch.action.explain.ExplainAction;
 import org.elasticsearch.action.explain.ExplainRequest;
 import org.elasticsearch.action.explain.ExplainRequestBuilder;
@@ -382,6 +386,21 @@ public abstract class AbstractClient implements Client {
         return new CountRequestBuilder(this).setIndices(indices);
     }
 
+    @Override
+    public ActionFuture<ExistsResponse> exists(final ExistsRequest request) {
+        return execute(ExistsAction.INSTANCE, request);
+    }
+
+    @Override
+    public void exists(final ExistsRequest request, final ActionListener<ExistsResponse> listener) {
+        execute(ExistsAction.INSTANCE, request, listener);
+    }
+
+    @Override
+    public ExistsRequestBuilder prepareExists(String... indices) {
+        return new ExistsRequestBuilder(this).setIndices(indices);
+    }
+
     @Override
     public ActionFuture<SuggestResponse> suggest(final SuggestRequest request) {
         return execute(SuggestAction.INSTANCE, request);

+ 1 - 0
src/main/java/org/elasticsearch/rest/action/RestActionModule.java

@@ -23,6 +23,7 @@ import com.google.common.collect.Lists;
 import org.elasticsearch.common.inject.AbstractModule;
 import org.elasticsearch.common.inject.multibindings.Multibinder;
 import org.elasticsearch.rest.BaseRestHandler;
+import org.elasticsearch.rest.action.exists.RestExistsAction;
 import org.elasticsearch.rest.action.admin.cluster.health.RestClusterHealthAction;
 import org.elasticsearch.rest.action.admin.cluster.node.hotthreads.RestNodesHotThreadsAction;
 import org.elasticsearch.rest.action.admin.cluster.node.info.RestNodesInfoAction;

+ 81 - 0
src/main/java/org/elasticsearch/rest/action/exists/RestExistsAction.java

@@ -0,0 +1,81 @@
+/*
+ * 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.exists;
+
+import org.elasticsearch.action.exists.ExistsRequest;
+import org.elasticsearch.action.exists.ExistsResponse;
+import org.elasticsearch.action.support.IndicesOptions;
+import org.elasticsearch.action.support.QuerySourceBuilder;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.rest.*;
+import org.elasticsearch.rest.action.support.RestActions;
+import org.elasticsearch.rest.action.support.RestBuilderListener;
+
+import static org.elasticsearch.action.exists.ExistsRequest.DEFAULT_MIN_SCORE;
+import static org.elasticsearch.rest.RestStatus.NOT_FOUND;
+import static org.elasticsearch.rest.RestStatus.OK;
+
+/**
+ * Action for /_search/exists endpoint
+ */
+public class RestExistsAction extends BaseRestHandler {
+
+    public RestExistsAction(Settings settings, Client client) {
+        super(settings, client);
+    }
+
+    @Override
+    public void handleRequest(final RestRequest request, final RestChannel channel, final Client client) {
+        final ExistsRequest existsRequest = new ExistsRequest(Strings.splitStringByCommaToArray(request.param("index")));
+        existsRequest.indicesOptions(IndicesOptions.fromRequest(request, existsRequest.indicesOptions()));
+        existsRequest.listenerThreaded(false);
+        if (request.hasContent()) {
+            existsRequest.source(request.content(), request.contentUnsafe());
+        } else {
+            String source = request.param("source");
+            if (source != null) {
+                existsRequest.source(source);
+            } else {
+                QuerySourceBuilder querySourceBuilder = RestActions.parseQuerySource(request);
+                if (querySourceBuilder != null) {
+                    existsRequest.source(querySourceBuilder);
+                }
+            }
+        }
+        existsRequest.routing(request.param("routing"));
+        existsRequest.minScore(request.paramAsFloat("min_score", DEFAULT_MIN_SCORE));
+        existsRequest.types(Strings.splitStringByCommaToArray(request.param("type")));
+        existsRequest.preference(request.param("preference"));
+
+        client.exists(existsRequest, new RestBuilderListener<ExistsResponse>(channel) {
+            @Override
+            public RestResponse buildResponse(ExistsResponse response, XContentBuilder builder) throws Exception {
+                RestStatus status = response.exists() ? OK : NOT_FOUND;
+                builder.startObject();
+                builder.field("exists", response.exists());
+                builder.endObject();
+                return new BytesRestResponse(status, builder);
+            }
+        });
+    }
+}

+ 9 - 0
src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java

@@ -33,6 +33,7 @@ import org.elasticsearch.rest.BaseRestHandler;
 import org.elasticsearch.rest.RestChannel;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.exists.RestExistsAction;
 import org.elasticsearch.rest.action.support.RestStatusToXContentListener;
 import org.elasticsearch.search.Scroll;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
@@ -65,6 +66,14 @@ public class RestSearchAction extends BaseRestHandler {
         controller.registerHandler(POST, "/{index}/_search/template", this);
         controller.registerHandler(GET, "/{index}/{type}/_search/template", this);
         controller.registerHandler(POST, "/{index}/{type}/_search/template", this);
+
+        RestExistsAction restExistsAction = new RestExistsAction(settings, client);
+        controller.registerHandler(GET, "/_search/exists", restExistsAction);
+        controller.registerHandler(POST, "/_search/exists", restExistsAction);
+        controller.registerHandler(GET, "/{index}/_search/exists", restExistsAction);
+        controller.registerHandler(POST, "/{index}/_search/exists", restExistsAction);
+        controller.registerHandler(GET, "/{index}/{type}/_search/exists", restExistsAction);
+        controller.registerHandler(POST, "/{index}/{type}/_search/exists", restExistsAction);
     }
 
     @Override

+ 131 - 0
src/test/java/org/elasticsearch/exists/SimpleExistsTests.java

@@ -0,0 +1,131 @@
+/*
+ * 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.exists;
+
+import org.elasticsearch.action.exists.ExistsResponse;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.junit.Test;
+
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
+import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertExists;
+
+public class SimpleExistsTests extends ElasticsearchIntegrationTest {
+
+
+    @Test
+    public void testExistsRandomPreference() throws Exception {
+        createIndex("test");
+        indexRandom(true, client().prepareIndex("test", "type", "1").setSource("field", "value"),
+                client().prepareIndex("test", "type", "2").setSource("field", "value"),
+                client().prepareIndex("test", "type", "3").setSource("field", "value"),
+                client().prepareIndex("test", "type", "4").setSource("field", "value"),
+                client().prepareIndex("test", "type", "5").setSource("field", "value"),
+                client().prepareIndex("test", "type", "6").setSource("field", "value"));
+
+        int iters = scaledRandomIntBetween(10, 100);
+        for (int i = 0; i < iters; i++) {
+
+            String randomPreference = randomUnicodeOfLengthBetween(0, 4);
+            // randomPreference should not start with '_' (reserved for known preference types (e.g. _shards, _primary)
+            while (randomPreference.startsWith("_")) {
+                randomPreference = randomUnicodeOfLengthBetween(0, 4);
+            }
+            // id is not indexed, but lets see that we automatically convert to
+            ExistsResponse existsResponse = client().prepareExists().setQuery(QueryBuilders.matchAllQuery()).setPreference(randomPreference).get();
+            assertExists(existsResponse, true);
+        }
+    }
+
+
+    @Test
+    public void simpleIpTests() throws Exception {
+        createIndex("test");
+
+        client().admin().indices().preparePutMapping("test").setType("type1")
+                .setSource(XContentFactory.jsonBuilder().startObject().startObject("type1").startObject("properties")
+                        .startObject("from").field("type", "ip").endObject()
+                        .startObject("to").field("type", "ip").endObject()
+                        .endObject().endObject().endObject())
+                .execute().actionGet();
+
+        client().prepareIndex("test", "type1", "1").setSource("from", "192.168.0.5", "to", "192.168.0.10").setRefresh(true).execute().actionGet();
+
+        ExistsResponse existsResponse = client().prepareExists()
+                .setQuery(boolQuery().must(rangeQuery("from").lt("192.168.0.7")).must(rangeQuery("to").gt("192.168.0.7"))).get();
+
+        assertExists(existsResponse, true);
+
+        existsResponse = client().prepareExists().setQuery(boolQuery().must(rangeQuery("from").lt("192.168.0.4")).must(rangeQuery("to").gt("192.168.0.11"))).get();
+
+        assertExists(existsResponse, false);
+    }
+
+    @Test
+    public void simpleIdTests() {
+        createIndex("test");
+
+        client().prepareIndex("test", "type", "XXX1").setSource("field", "value").setRefresh(true).execute().actionGet();
+        // id is not indexed, but lets see that we automatically convert to
+        ExistsResponse existsResponse = client().prepareExists().setQuery(QueryBuilders.termQuery("_id", "XXX1")).execute().actionGet();
+        assertExists(existsResponse, true);
+
+        existsResponse = client().prepareExists().setQuery(QueryBuilders.queryString("_id:XXX1")).execute().actionGet();
+        assertExists(existsResponse, true);
+
+        existsResponse = client().prepareExists().setQuery(QueryBuilders.prefixQuery("_id", "XXX")).execute().actionGet();
+        assertExists(existsResponse, true);
+
+        existsResponse = client().prepareExists().setQuery(QueryBuilders.queryString("_id:XXX*").lowercaseExpandedTerms(false)).execute().actionGet();
+        assertExists(existsResponse, true);
+    }
+
+    @Test
+    public void simpleDateMathTests() throws Exception {
+        createIndex("test");
+        client().prepareIndex("test", "type1", "1").setSource("field", "2010-01-05T02:00").execute().actionGet();
+        client().prepareIndex("test", "type1", "2").setSource("field", "2010-01-06T02:00").execute().actionGet();
+        ensureGreen();
+        refresh();
+        ExistsResponse existsResponse = client().prepareExists("test").setQuery(QueryBuilders.rangeQuery("field").gte("2010-01-03||+2d").lte("2010-01-04||+2d")).execute().actionGet();
+        assertExists(existsResponse, true);
+
+        existsResponse = client().prepareExists("test").setQuery(QueryBuilders.queryString("field:[2010-01-03||+2d TO 2010-01-04||+2d]")).execute().actionGet();
+        assertExists(existsResponse, true);
+    }
+
+    @Test
+    public void simpleNonExistenceTests() throws Exception {
+        createIndex("test");
+        client().prepareIndex("test", "type1", "1").setSource("field", "2010-01-05T02:00").execute().actionGet();
+        client().prepareIndex("test", "type1", "2").setSource("field", "2010-01-06T02:00").execute().actionGet();
+        client().prepareIndex("test", "type", "XXX1").setSource("field", "value").execute().actionGet();
+        ensureGreen();
+        refresh();
+        ExistsResponse existsResponse = client().prepareExists("test").setQuery(QueryBuilders.rangeQuery("field").gte("2010-01-07||+2d").lte("2010-01-21||+2d")).execute().actionGet();
+        assertExists(existsResponse, false);
+
+        existsResponse = client().prepareExists("test").setQuery(QueryBuilders.queryString("_id:XXY*").lowercaseExpandedTerms(false)).execute().actionGet();
+        assertExists(existsResponse, false);
+    }
+
+}

+ 7 - 0
src/test/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java

@@ -44,6 +44,7 @@ import org.elasticsearch.action.admin.indices.delete.DeleteIndexResponse;
 import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesResponse;
 import org.elasticsearch.action.bulk.BulkResponse;
 import org.elasticsearch.action.count.CountResponse;
+import org.elasticsearch.action.exists.ExistsResponse;
 import org.elasticsearch.action.get.GetResponse;
 import org.elasticsearch.action.percolate.PercolateResponse;
 import org.elasticsearch.action.search.SearchPhaseExecutionException;
@@ -171,6 +172,12 @@ public class ElasticsearchAssertions {
         }
         assertVersionSerializable(countResponse);
     }
+    public static void assertExists(ExistsResponse existsResponse, boolean expected) {
+        if (existsResponse.exists() != expected) {
+            fail("Exist is " + existsResponse.exists() + " but " + expected + " was expected " + formatShardStatus(existsResponse));
+        }
+        assertVersionSerializable(existsResponse);
+    }
 
     public static void assertMatchCount(PercolateResponse percolateResponse, long expectedHitCount) {
         if (percolateResponse.getCount() != expectedHitCount) {