diff --git a/docs/reference/query-dsl/filters/indices-filter.asciidoc b/docs/reference/query-dsl/filters/indices-filter.asciidoc new file mode 100644 index 00000000000..48970c7e61a --- /dev/null +++ b/docs/reference/query-dsl/filters/indices-filter.asciidoc @@ -0,0 +1,38 @@ +[[query-dsl-indices-filter]] +=== Indices Filter + +The `indices` filter can be used when executed across multiple indices, +allowing to have a filter that executes only when executed on an index +that matches a specific list of indices, and another filter that executes +when it is executed on an index that does not match the listed indices. + +[source,js] +-------------------------------------------------- +{ + "indices" : { + "indices" : ["index1", "index2"], + "filter" : { + "term" : { "tag" : "wow" } + }, + "no_match_filter" : { + "term" : { "tag" : "kow" } + } + } +} +-------------------------------------------------- + +You can use the `index` field to provide a single index. + +`no_match_filter` can also have "string" value of `none` (to match no +documents), and `all` (to match all). Defaults to `all`. + +`filter` is mandatory. You must provide the indices. +It is forbidden to omit or to give `indices` or `index` multiple times, +or to give both. + +Please note that the fields order is important: If the indices are +provided before `filter` or `no_match_filter`, the filter parsing is +skipped altogether. +For instance, this feature is useful to prevent a query that runs +against multiple indices to fail because of a missing type. +See `has_child`, `has_parent`, `top_children` and `nested`. diff --git a/docs/reference/query-dsl/queries/indices-query.asciidoc b/docs/reference/query-dsl/queries/indices-query.asciidoc index 8ef16a6c6b7..bf36c515847 100644 --- a/docs/reference/query-dsl/queries/indices-query.asciidoc +++ b/docs/reference/query-dsl/queries/indices-query.asciidoc @@ -21,5 +21,18 @@ when it is executed on an index that does not match the listed indices. } -------------------------------------------------- +You can use the `index` field to provide a single index. + `no_match_query` can also have "string" value of `none` (to match no -documents), and `all` (to match all). +documents), and `all` (to match all). Defaults to `all`. + +`query` is mandatory. You must provide the indices. +It is forbidden to omit or to give `indices` or `index` multiple times, +or to give both. + +Please note that the fields order is important: If the indices are +provided before `query` or `no_match_query`, the query parsing is +skipped altogether. +For instance, this feature is useful to prevent a query that runs +against multiple indices to fail because of a missing type. +See `has_child`, `has_parent`, `top_children` and `nested`. diff --git a/src/main/java/org/elasticsearch/index/query/IndicesFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/IndicesFilterBuilder.java index 30ba5c1a863..a19a352193d 100644 --- a/src/main/java/org/elasticsearch/index/query/IndicesFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/IndicesFilterBuilder.java @@ -70,9 +70,9 @@ public class IndicesFilterBuilder extends BaseFilterBuilder { @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(IndicesFilterParser.NAME); + builder.field("indices", indices); builder.field("filter"); filterBuilder.toXContent(builder, params); - builder.field("indices", indices); if (noMatchFilter != null) { builder.field("no_match_filter"); noMatchFilter.toXContent(builder, params); diff --git a/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java b/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java index aa29d61e58d..282465d10a9 100644 --- a/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/IndicesFilterParser.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.query; -import com.google.common.collect.Sets; import org.apache.lucene.search.Filter; import org.elasticsearch.action.support.IgnoreIndices; import org.elasticsearch.cluster.ClusterService; @@ -31,7 +30,9 @@ import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; -import java.util.Set; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; /** */ @@ -57,26 +58,55 @@ public class IndicesFilterParser implements FilterParser { XContentParser parser = parseContext.parser(); Filter filter = null; + Filter noMatchFilter = Queries.MATCH_ALL_FILTER; + Filter chosenFilter = null; boolean filterFound = false; - Set indices = Sets.newHashSet(); + boolean indicesFound = false; + boolean matchesConcreteIndices = false; String currentFieldName = null; XContentParser.Token token; - Filter noMatchFilter = Queries.MATCH_ALL_FILTER; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.START_OBJECT) { if ("filter".equals(currentFieldName)) { filterFound = true; - filter = parseContext.parseInnerFilter(); + if (indicesFound) { + // Because we know the indices, we can either skip, or parse and use the query + if (matchesConcreteIndices) { + filter = parseContext.parseInnerFilter(); + chosenFilter = filter; + } else { + parseContext.parser().skipChildren(); // skip the filter object without parsing it into a Filter + } + } else { + // We do not know the indices, we must parse the query + filter = parseContext.parseInnerFilter(); + } } else if ("no_match_filter".equals(currentFieldName)) { - noMatchFilter = parseContext.parseInnerFilter(); + if (indicesFound) { + // Because we know the indices, we can either skip, or parse and use the query + if (!matchesConcreteIndices) { + noMatchFilter = parseContext.parseInnerFilter(); + chosenFilter = noMatchFilter; + } else { + parseContext.parser().skipChildren(); // skip the filter object without parsing it into a Filter + } + } else { + // We do not know the indices, we must parse the query + noMatchFilter = parseContext.parseInnerFilter(); + } } else { throw new QueryParsingException(parseContext.index(), "[indices] filter does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("indices".equals(currentFieldName)) { + if (indicesFound) { + throw new QueryParsingException(parseContext.index(), "[indices] indices already specified"); + } + indicesFound = true; + Collection indices = new ArrayList(); while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { String value = parser.textOrNull(); if (value == null) { @@ -84,12 +114,17 @@ public class IndicesFilterParser implements FilterParser { } indices.add(value); } + matchesConcreteIndices = matchesIndices(parseContext, getConcreteIndices(indices)); } else { throw new QueryParsingException(parseContext.index(), "[indices] filter does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("index".equals(currentFieldName)) { - indices.add(parser.text()); + if (indicesFound) { + throw new QueryParsingException(parseContext.index(), "[indices] indices already specified"); + } + indicesFound = true; + matchesConcreteIndices = matchesIndices(parseContext, getConcreteIndices(Arrays.asList(parser.text()))); } else if ("no_match_filter".equals(currentFieldName)) { String type = parser.text(); if ("all".equals(type)) { @@ -97,6 +132,11 @@ public class IndicesFilterParser implements FilterParser { } else if ("none".equals(type)) { noMatchFilter = Queries.MATCH_NO_FILTER; } + if (indicesFound) { + if (!matchesConcreteIndices) { + chosenFilter = noMatchFilter; + } + } } else { throw new QueryParsingException(parseContext.index(), "[indices] filter does not support [" + currentFieldName + "]"); } @@ -105,25 +145,38 @@ public class IndicesFilterParser implements FilterParser { if (!filterFound) { throw new QueryParsingException(parseContext.index(), "[indices] requires 'filter' element"); } - if (indices.isEmpty()) { + if (!indicesFound) { throw new QueryParsingException(parseContext.index(), "[indices] requires 'indices' element"); } - if (filter == null) { - return null; + if (chosenFilter == null) { + // Indices were not provided before we encountered the queries, which we hence parsed + // We must now make a choice + if (matchesConcreteIndices) { + chosenFilter = filter; + } else { + chosenFilter = noMatchFilter; + } } + return chosenFilter; + } + + protected String[] getConcreteIndices(Collection indices) { String[] concreteIndices = indices.toArray(new String[indices.size()]); if (clusterService != null) { MetaData metaData = clusterService.state().metaData(); concreteIndices = metaData.concreteIndices(indices.toArray(new String[indices.size()]), IgnoreIndices.MISSING, true); } + return concreteIndices; + } + protected boolean matchesIndices(QueryParseContext parseContext, String[] concreteIndices) { for (String index : concreteIndices) { if (Regex.simpleMatch(index, parseContext.index().name())) { - return filter; + return true; } } - return noMatchFilter; + return false; } } diff --git a/src/main/java/org/elasticsearch/index/query/IndicesQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/IndicesQueryBuilder.java index 8a2038455be..269434ed9d9 100644 --- a/src/main/java/org/elasticsearch/index/query/IndicesQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/IndicesQueryBuilder.java @@ -70,9 +70,9 @@ public class IndicesQueryBuilder extends BaseQueryBuilder { @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(IndicesQueryParser.NAME); + builder.field("indices", indices); builder.field("query"); queryBuilder.toXContent(builder, params); - builder.field("indices", indices); if (noMatchQuery != null) { builder.field("no_match_query"); noMatchQuery.toXContent(builder, params); diff --git a/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java b/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java index 801a727152b..b2544c8e876 100644 --- a/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/IndicesQueryParser.java @@ -19,20 +19,20 @@ package org.elasticsearch.index.query; -import com.google.common.collect.Sets; import org.apache.lucene.search.Query; import org.elasticsearch.action.support.IgnoreIndices; import org.elasticsearch.cluster.ClusterService; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.lucene.search.MatchNoDocsQuery; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; -import java.util.Set; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; /** */ @@ -58,27 +58,56 @@ public class IndicesQueryParser implements QueryParser { XContentParser parser = parseContext.parser(); Query query = null; + Query noMatchQuery = Queries.newMatchAllQuery(); + Query chosenQuery = null; boolean queryFound = false; - Set indices = Sets.newHashSet(); + boolean indicesFound = false; + boolean matchesConcreteIndices = false; String queryName = null; String currentFieldName = null; XContentParser.Token token; - Query noMatchQuery = Queries.newMatchAllQuery(); while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token == XContentParser.Token.START_OBJECT) { if ("query".equals(currentFieldName)) { - query = parseContext.parseInnerQuery(); queryFound = true; + if (indicesFound) { + // Because we know the indices, we can either skip, or parse and use the query + if (matchesConcreteIndices) { + query = parseContext.parseInnerQuery(); + chosenQuery = query; + } else { + parseContext.parser().skipChildren(); // skip the query object without parsing it into a Query + } + } else { + // We do not know the indices, we must parse the query + query = parseContext.parseInnerQuery(); + } } else if ("no_match_query".equals(currentFieldName)) { - noMatchQuery = parseContext.parseInnerQuery(); + if (indicesFound) { + // Because we know the indices, we can either skip, or parse and use the query + if (!matchesConcreteIndices) { + noMatchQuery = parseContext.parseInnerQuery(); + chosenQuery = noMatchQuery; + } else { + parseContext.parser().skipChildren(); // skip the query object without parsing it into a Query + } + } else { + // We do not know the indices, we must parse the query + noMatchQuery = parseContext.parseInnerQuery(); + } } else { throw new QueryParsingException(parseContext.index(), "[indices] query does not support [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("indices".equals(currentFieldName)) { + if (indicesFound) { + throw new QueryParsingException(parseContext.index(), "[indices] indices already specified"); + } + indicesFound = true; + Collection indices = new ArrayList(); while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { String value = parser.textOrNull(); if (value == null) { @@ -86,12 +115,17 @@ public class IndicesQueryParser implements QueryParser { } indices.add(value); } + matchesConcreteIndices = matchesIndices(parseContext, getConcreteIndices(indices)); } else { throw new QueryParsingException(parseContext.index(), "[indices] query does not support [" + currentFieldName + "]"); } } else if (token.isValue()) { if ("index".equals(currentFieldName)) { - indices.add(parser.text()); + if (indicesFound) { + throw new QueryParsingException(parseContext.index(), "[indices] indices already specified"); + } + indicesFound = true; + matchesConcreteIndices = matchesIndices(parseContext, getConcreteIndices(Arrays.asList(parser.text()))); } else if ("no_match_query".equals(currentFieldName)) { String type = parser.text(); if ("all".equals(type)) { @@ -99,6 +133,11 @@ public class IndicesQueryParser implements QueryParser { } else if ("none".equals(type)) { noMatchQuery = Queries.newMatchNoDocsQuery(); } + if (indicesFound) { + if (!matchesConcreteIndices) { + chosenQuery = noMatchQuery; + } + } } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { @@ -109,30 +148,42 @@ public class IndicesQueryParser implements QueryParser { if (!queryFound) { throw new QueryParsingException(parseContext.index(), "[indices] requires 'query' element"); } - if (query == null) { - return null; - } - if (indices.isEmpty()) { + if (!indicesFound) { throw new QueryParsingException(parseContext.index(), "[indices] requires 'indices' element"); } + if (chosenQuery == null) { + // Indices were not provided before we encountered the queries, which we hence parsed + // We must now make a choice + if (matchesConcreteIndices) { + chosenQuery = query; + } else { + chosenQuery = noMatchQuery; + } + } + + if (queryName != null && chosenQuery != null) { + parseContext.addNamedQuery(queryName, chosenQuery); + } + + return chosenQuery; + } + + protected String[] getConcreteIndices(Collection indices) { String[] concreteIndices = indices.toArray(new String[indices.size()]); if (clusterService != null) { MetaData metaData = clusterService.state().metaData(); concreteIndices = metaData.concreteIndices(indices.toArray(new String[indices.size()]), IgnoreIndices.MISSING, true); } + return concreteIndices; + } + protected boolean matchesIndices(QueryParseContext parseContext, String[] concreteIndices) { for (String index : concreteIndices) { if (Regex.simpleMatch(index, parseContext.index().name())) { - if (queryName != null) { - parseContext.addNamedQuery(queryName, query); - } - return query; + return true; } } - if (queryName != null) { - parseContext.addNamedQuery(queryName, noMatchQuery); - } - return noMatchQuery; + return false; } } diff --git a/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java b/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java index 337c05f1d1f..f724c9af0f9 100644 --- a/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java +++ b/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java @@ -1890,4 +1890,178 @@ public class SimpleQueryTests extends ElasticsearchIntegrationTest { .setQuery(QueryBuilders.multiMatchQuery("value2", "field2^2").lenient(true)).get(); assertHitCount(searchResponse, 1l); } + + @Test + public void testIndicesQuery() throws Exception { + createIndex("index1", "index2"); + ensureGreen(); + + client().prepareIndex("index1", "type1").setId("1").setSource("text", "value").get(); + client().prepareIndex("index2", "type2").setId("2").setSource("text", "value").get(); + refresh(); + + SearchResponse response = client().prepareSearch("index1", "index2") + .setQuery(indicesQuery(matchQuery("text", "value"), "index1") + .noMatchQuery(matchQuery("text", "value"))).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("index1", "index2") + .setQuery(indicesQuery(matchQuery("text", "value"), "index1")).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("index1", "index2") + .setQuery(indicesQuery(matchQuery("text", "value"), "index1") + .noMatchQuery("all")).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("index1", "index2") + .setQuery(indicesQuery(matchQuery("text", "value"), "index1") + .noMatchQuery("none")).get(); + assertHitCount(response, 1l); + assertThat(response.getHits().getAt(0).getId(), equalTo("1")); + } + + @Test + public void testIndicesFilter() throws Exception { + createIndex("index1", "index2"); + ensureGreen(); + + client().prepareIndex("index1", "type1").setId("1").setSource("text", "value").get(); + client().prepareIndex("index2", "type2").setId("2").setSource("text", "value").get(); + refresh(); + + SearchResponse response = client().prepareSearch("index1", "index2") + .setFilter(indicesFilter(termFilter("text", "value"), "index1") + .noMatchFilter(termFilter("text", "value"))).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("index1", "index2") + .setFilter(indicesFilter(termFilter("text", "value"), "index1")).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("index1", "index2") + .setFilter(indicesFilter(termFilter("text", "value"), "index1") + .noMatchFilter("all")).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("index1", "index2") + .setFilter(indicesFilter(termFilter("text", "value"), "index1") + .noMatchFilter("none")).get(); + assertHitCount(response, 1l); + assertThat(response.getHits().getAt(0).getId(), equalTo("1")); + } + + @Test // https://github.com/elasticsearch/elasticsearch/issues/2416 + public void testIndicesQueryHideParsingExceptions() throws Exception { + client().admin().indices().prepareCreate("simple") + .addMapping("lone", jsonBuilder().startObject().startObject("lone").endObject().endObject()) + .get(); + client().admin().indices().prepareCreate("related") + .addMapping("parent", jsonBuilder().startObject().startObject("parent").endObject().endObject()) + .addMapping("child", jsonBuilder().startObject().startObject("child").startObject("_parent").field("type", "parent") + .endObject().endObject().endObject()) + .get(); + ensureGreen(); + + client().prepareIndex("simple", "lone").setId("1").setSource("text", "value").get(); + client().prepareIndex("related", "parent").setId("2").setSource("text", "parent").get(); + client().prepareIndex("related", "child").setId("3").setParent("2").setSource("text", "value").get(); + refresh(); + + SearchResponse response = client().prepareSearch("related") + .setQuery(hasChildQuery("child", matchQuery("text", "value"))).get(); + assertHitCount(response, 1l); + assertThat(response.getHits().getAt(0).getId(), equalTo("2")); + + response = client().prepareSearch("simple") + .setQuery(matchQuery("text", "value")).get(); + assertHitCount(response, 1l); + assertThat(response.getHits().getAt(0).getId(), equalTo("1")); + + try { + client().prepareSearch("simple") + .setQuery(hasChildQuery("child", matchQuery("text", "value"))).get(); + fail("Should have failed with a SearchPhaseExecutionException because all shards failed with a nested QueryParsingException"); + // If no failure happens, the HasChildQuery may have changed behavior when provided with wrong types + } catch (SearchPhaseExecutionException e) { + // There is no easy way to ensure we got a QueryParsingException + } + + response = client().prepareSearch("related", "simple") + .setQuery(indicesQuery(matchQuery("text", "parent"), "related") + .noMatchQuery(matchQuery("text", "value"))).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("related", "simple") + .setQuery(indicesQuery(hasChildQuery("child", matchQuery("text", "value")), "related") + .noMatchQuery(matchQuery("text", "value"))).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + } + + @Test // https://github.com/elasticsearch/elasticsearch/issues/2416 + public void testIndicesFilterHideParsingExceptions() throws Exception { + client().admin().indices().prepareCreate("simple") + .addMapping("lone", jsonBuilder().startObject().startObject("lone").endObject().endObject()) + .get(); + client().admin().indices().prepareCreate("related") + .addMapping("parent", jsonBuilder().startObject().startObject("parent").endObject().endObject()) + .addMapping("child", jsonBuilder().startObject().startObject("child").startObject("_parent").field("type", "parent") + .endObject().endObject().endObject()) + .get(); + ensureGreen(); + + client().prepareIndex("simple", "lone").setId("1").setSource("text", "value").get(); + client().prepareIndex("related", "parent").setId("2").setSource("text", "parent").get(); + client().prepareIndex("related", "child").setId("3").setParent("2").setSource("text", "value").get(); + refresh(); + + SearchResponse response = client().prepareSearch("related") + .setFilter(hasChildFilter("child", termFilter("text", "value"))).get(); + assertHitCount(response, 1l); + assertThat(response.getHits().getAt(0).getId(), equalTo("2")); + + response = client().prepareSearch("simple") + .setFilter(termFilter("text", "value")).get(); + assertHitCount(response, 1l); + assertThat(response.getHits().getAt(0).getId(), equalTo("1")); + + try { + client().prepareSearch("simple") + .setFilter(hasChildFilter("child", termFilter("text", "value"))).get(); + fail("Should have failed with a SearchPhaseExecutionException because all shards failed with a nested QueryParsingException"); + // If no failure happens, the HasChildQuery may have changed behavior when provided with wrong types + } catch (SearchPhaseExecutionException e) { + // There is no easy way to ensure we got a QueryParsingException + } + + response = client().prepareSearch("related", "simple") + .setFilter(indicesFilter(termFilter("text", "parent"), "related") + .noMatchFilter(termFilter("text", "value"))).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + + response = client().prepareSearch("related", "simple") + .setFilter(indicesFilter(hasChildFilter("child", termFilter("text", "value")), "related") + .noMatchFilter(termFilter("text", "value"))).get(); + assertHitCount(response, 2l); + assertThat(response.getHits().getAt(0).getId(), either(equalTo("1")).or(equalTo("2"))); + assertThat(response.getHits().getAt(1).getId(), either(equalTo("1")).or(equalTo("2"))); + } }