diff --git a/docs/reference/query-dsl/queries.asciidoc b/docs/reference/query-dsl/queries.asciidoc index d56d2c719f1..20ce2789f7c 100644 --- a/docs/reference/query-dsl/queries.asciidoc +++ b/docs/reference/query-dsl/queries.asciidoc @@ -52,6 +52,8 @@ include::queries/range-query.asciidoc[] include::queries/regexp-query.asciidoc[] +include::queries/span-containing-query.asciidoc[] + include::queries/span-first-query.asciidoc[] include::queries/span-multi-term-query.asciidoc[] @@ -64,6 +66,8 @@ include::queries/span-or-query.asciidoc[] include::queries/span-term-query.asciidoc[] +include::queries/span-within-query.asciidoc[] + include::queries/term-query.asciidoc[] include::queries/terms-query.asciidoc[] diff --git a/docs/reference/query-dsl/queries/span-containing-query.asciidoc b/docs/reference/query-dsl/queries/span-containing-query.asciidoc new file mode 100644 index 00000000000..965bf855b6f --- /dev/null +++ b/docs/reference/query-dsl/queries/span-containing-query.asciidoc @@ -0,0 +1,29 @@ +[[query-dsl-span-containing-query]] +=== Span Containing Query + +Returns matches which enclose another span query. The span containing +query maps to Lucene `SpanContainingQuery`. Here is an example: + +[source,js] +-------------------------------------------------- +{ + "span_containing" : { + "little" : { + "span_term" : { "field1" : "foo" } + }, + "big" : { + "span_near" : { + "clauses" : [ + { "span_term" : { "field1" : "bar" } }, + { "span_term" : { "field1" : "baz" } } + ], + "slop" : 5, + "in_order" : true + } + } + } +} +-------------------------------------------------- + +The `big` and `little` clauses can be any span type query. Matching +spans from `big` that contain matches from `little` are returned. diff --git a/docs/reference/query-dsl/queries/span-within-query.asciidoc b/docs/reference/query-dsl/queries/span-within-query.asciidoc new file mode 100644 index 00000000000..dc5c4bbfdfd --- /dev/null +++ b/docs/reference/query-dsl/queries/span-within-query.asciidoc @@ -0,0 +1,29 @@ +[[query-dsl-span-within-query]] +=== Span Within Query + +Returns matches which are enclosed inside another span query. The span within +query maps to Lucene `SpanWithinQuery`. Here is an example: + +[source,js] +-------------------------------------------------- +{ + "span_within" : { + "little" : { + "span_term" : { "field1" : "foo" } + }, + "big" : { + "span_near" : { + "clauses" : [ + { "span_term" : { "field1" : "bar" } }, + { "span_term" : { "field1" : "baz" } } + ], + "slop" : 5, + "in_order" : true + } + } + } +} +-------------------------------------------------- + +The `big` and `little` clauses can be any span type query. Matching +spans from `little` that are enclosed within `big` are returned. diff --git a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java index 415544f3288..f383dc33ac6 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java @@ -318,6 +318,16 @@ public abstract class QueryBuilders { return new SpanOrQueryBuilder(); } + /** Creates a new {@code span_within} builder. */ + public static SpanWithinQueryBuilder spanWithinQuery() { + return new SpanWithinQueryBuilder(); + } + + /** Creates a new {@code span_containing} builder. */ + public static SpanContainingQueryBuilder spanContainingQuery() { + return new SpanContainingQueryBuilder(); + } + /** * Creates a {@link SpanQueryBuilder} which allows having a sub query * which implements {@link MultiTermQueryBuilder}. This is useful for diff --git a/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java new file mode 100644 index 00000000000..6fd2dee013a --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java @@ -0,0 +1,92 @@ +/* + * 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.index.query; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Builder for {@link SpanContainingQuery}. + */ +public class SpanContainingQueryBuilder extends BaseQueryBuilder implements SpanQueryBuilder, BoostableQueryBuilder { + + private SpanQueryBuilder big; + private SpanQueryBuilder little; + private float boost = -1; + private String queryName; + + /** + * Sets the little clause, it must be contained within {@code big} for a match. + */ + public SpanContainingQueryBuilder little(SpanQueryBuilder clause) { + this.little = clause; + return this; + } + + /** + * Sets the big clause, it must enclose {@code little} for a match. + */ + public SpanContainingQueryBuilder big(SpanQueryBuilder clause) { + this.big = clause; + return this; + } + + @Override + public SpanContainingQueryBuilder boost(float boost) { + this.boost = boost; + return this; + } + + /** + * Sets the query name for the filter that can be used when searching for matched_filters per hit. + */ + public SpanContainingQueryBuilder queryName(String queryName) { + this.queryName = queryName; + return this; + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + if (big == null) { + throw new IllegalArgumentException("Must specify big clause when building a span_containing query"); + } + if (little == null) { + throw new IllegalArgumentException("Must specify little clause when building a span_containing query"); + } + builder.startObject(SpanContainingQueryParser.NAME); + + builder.field("big"); + big.toXContent(builder, params); + + builder.field("little"); + little.toXContent(builder, params); + + if (boost != -1) { + builder.field("boost", boost); + } + + if (queryName != null) { + builder.field("_name", queryName); + } + + builder.endObject(); + } +} diff --git a/src/main/java/org/elasticsearch/index/query/SpanContainingQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanContainingQueryParser.java new file mode 100644 index 00000000000..63e312bf384 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/SpanContainingQueryParser.java @@ -0,0 +1,100 @@ +/* + * 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.index.query; + +import org.apache.lucene.search.Query; +import org.apache.lucene.search.spans.SpanContainingQuery; +import org.apache.lucene.search.spans.SpanQuery; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; + +/** + * Parser for {@link SpanContainingQuery} + */ +public class SpanContainingQueryParser implements QueryParser { + + public static final String NAME = "span_containing"; + + @Inject + public SpanContainingQueryParser() { + } + + @Override + public String[] names() { + return new String[]{NAME, Strings.toCamelCase(NAME)}; + } + + @Override + public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + float boost = 1.0f; + String queryName = null; + SpanQuery big = null; + SpanQuery little = null; + + String currentFieldName = null; + XContentParser.Token token; + 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 ("big".equals(currentFieldName)) { + Query query = parseContext.parseInnerQuery(); + if (!(query instanceof SpanQuery)) { + throw new QueryParsingException(parseContext, "span_containing [big] must be of type span query"); + } + big = (SpanQuery) query; + } else if ("little".equals(currentFieldName)) { + Query query = parseContext.parseInnerQuery(); + if (!(query instanceof SpanQuery)) { + throw new QueryParsingException(parseContext, "span_containing [little] must be of type span query"); + } + little = (SpanQuery) query; + } else { + throw new QueryParsingException(parseContext, "[span_containing] query does not support [" + currentFieldName + "]"); + } + } else if ("boost".equals(currentFieldName)) { + boost = parser.floatValue(); + } else if ("_name".equals(currentFieldName)) { + queryName = parser.text(); + } else { + throw new QueryParsingException(parseContext, "[span_containing] query does not support [" + currentFieldName + "]"); + } + } + + if (big == null) { + throw new QueryParsingException(parseContext, "span_containing must include [big]"); + } + if (little == null) { + throw new QueryParsingException(parseContext, "span_containing must include [little]"); + } + + Query query = new SpanContainingQuery(big, little); + query.setBoost(boost); + if (queryName != null) { + parseContext.addNamedQuery(queryName, query); + } + return query; + } +} diff --git a/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java new file mode 100644 index 00000000000..88e1538bff4 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java @@ -0,0 +1,92 @@ +/* + * 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.index.query; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Builder for {@link SpanWithinQuery}. + */ +public class SpanWithinQueryBuilder extends BaseQueryBuilder implements SpanQueryBuilder, BoostableQueryBuilder { + + private SpanQueryBuilder big; + private SpanQueryBuilder little; + private float boost = -1; + private String queryName; + + /** + * Sets the little clause, it must be contained within {@code big} for a match. + */ + public SpanWithinQueryBuilder little(SpanQueryBuilder clause) { + this.little = clause; + return this; + } + + /** + * Sets the big clause, it must enclose {@code little} for a match. + */ + public SpanWithinQueryBuilder big(SpanQueryBuilder clause) { + this.big = clause; + return this; + } + + @Override + public SpanWithinQueryBuilder boost(float boost) { + this.boost = boost; + return this; + } + + /** + * Sets the query name for the filter that can be used when searching for matched_filters per hit. + */ + public SpanWithinQueryBuilder queryName(String queryName) { + this.queryName = queryName; + return this; + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + if (big == null) { + throw new IllegalArgumentException("Must specify big clause when building a span_within query"); + } + if (little == null) { + throw new IllegalArgumentException("Must specify little clause when building a span_within query"); + } + builder.startObject(SpanWithinQueryParser.NAME); + + builder.field("big"); + big.toXContent(builder, params); + + builder.field("little"); + little.toXContent(builder, params); + + if (boost != -1) { + builder.field("boost", boost); + } + + if (queryName != null) { + builder.field("_name", queryName); + } + + builder.endObject(); + } +} diff --git a/src/main/java/org/elasticsearch/index/query/SpanWithinQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanWithinQueryParser.java new file mode 100644 index 00000000000..9194cbd2d0e --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/SpanWithinQueryParser.java @@ -0,0 +1,100 @@ +/* + * 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.index.query; + +import org.apache.lucene.search.Query; +import org.apache.lucene.search.spans.SpanQuery; +import org.apache.lucene.search.spans.SpanWithinQuery; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; + +/** + * Parser for {@link SpanWithinQuery} + */ +public class SpanWithinQueryParser implements QueryParser { + + public static final String NAME = "span_within"; + + @Inject + public SpanWithinQueryParser() { + } + + @Override + public String[] names() { + return new String[]{NAME, Strings.toCamelCase(NAME)}; + } + + @Override + public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + float boost = 1.0f; + String queryName = null; + SpanQuery big = null; + SpanQuery little = null; + + String currentFieldName = null; + XContentParser.Token token; + 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 ("big".equals(currentFieldName)) { + Query query = parseContext.parseInnerQuery(); + if (query instanceof SpanQuery == false) { + throw new QueryParsingException(parseContext, "span_within [big] must be of type span query"); + } + big = (SpanQuery) query; + } else if ("little".equals(currentFieldName)) { + Query query = parseContext.parseInnerQuery(); + if (query instanceof SpanQuery == false) { + throw new QueryParsingException(parseContext, "span_within [little] must be of type span query"); + } + little = (SpanQuery) query; + } else { + throw new QueryParsingException(parseContext, "[span_within] query does not support [" + currentFieldName + "]"); + } + } else if ("boost".equals(currentFieldName)) { + boost = parser.floatValue(); + } else if ("_name".equals(currentFieldName)) { + queryName = parser.text(); + } else { + throw new QueryParsingException(parseContext, "[span_within] query does not support [" + currentFieldName + "]"); + } + } + + if (big == null) { + throw new QueryParsingException(parseContext, "span_within must include [big]"); + } + if (little == null) { + throw new QueryParsingException(parseContext, "span_within must include [little]"); + } + + Query query = new SpanWithinQuery(big, little); + query.setBoost(boost); + if (queryName != null) { + parseContext.addNamedQuery(queryName, query); + } + return query; + } +} diff --git a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java index 14bbbc428f2..d70995ffebf 100644 --- a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java +++ b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java @@ -89,6 +89,8 @@ public class IndicesQueriesModule extends AbstractModule { qpBinders.addBinding().to(ConstantScoreQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(SpanTermQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(SpanNotQueryParser.class).asEagerSingleton(); + qpBinders.addBinding().to(SpanWithinQueryParser.class).asEagerSingleton(); + qpBinders.addBinding().to(SpanContainingQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(FieldMaskingSpanQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(SpanFirstQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(SpanNearQueryParser.class).asEagerSingleton(); diff --git a/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java b/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java index d37e5cf31c0..5c29fe57713 100644 --- a/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java +++ b/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java @@ -51,12 +51,14 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.search.spans.FieldMaskingSpanQuery; +import org.apache.lucene.search.spans.SpanContainingQuery; import org.apache.lucene.search.spans.SpanFirstQuery; import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper; import org.apache.lucene.search.spans.SpanNearQuery; import org.apache.lucene.search.spans.SpanNotQuery; import org.apache.lucene.search.spans.SpanOrQuery; import org.apache.lucene.search.spans.SpanTermQuery; +import org.apache.lucene.search.spans.SpanWithinQuery; import org.apache.lucene.spatial.prefix.IntersectsPrefixTreeFilter; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; @@ -131,11 +133,13 @@ import static org.elasticsearch.index.query.QueryBuilders.prefixQuery; import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery; import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; import static org.elasticsearch.index.query.QueryBuilders.regexpQuery; +import static org.elasticsearch.index.query.QueryBuilders.spanContainingQuery; import static org.elasticsearch.index.query.QueryBuilders.spanFirstQuery; import static org.elasticsearch.index.query.QueryBuilders.spanNearQuery; import static org.elasticsearch.index.query.QueryBuilders.spanNotQuery; import static org.elasticsearch.index.query.QueryBuilders.spanOrQuery; import static org.elasticsearch.index.query.QueryBuilders.spanTermQuery; +import static org.elasticsearch.index.query.QueryBuilders.spanWithinQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.index.query.QueryBuilders.termsQuery; import static org.elasticsearch.index.query.QueryBuilders.wildcardQuery; @@ -1434,6 +1438,50 @@ public class SimpleIndexQueryParserTests extends ElasticsearchSingleNodeTest { assertThat(((SpanTermQuery) spanNotQuery.getExclude()).getTerm(), equalTo(new Term("age", longToPrefixCoded(35, 0)))); } + @Test + public void testSpanWithinQueryBuilder() throws IOException { + IndexQueryParserService queryParser = queryParser(); + Query expectedQuery = new SpanWithinQuery(new SpanTermQuery(new Term("age", longToPrefixCoded(34, 0))), + new SpanTermQuery(new Term("age", longToPrefixCoded(35, 0)))); + Query actualQuery = queryParser.parse(spanWithinQuery() + .big(spanTermQuery("age", 34)) + .little(spanTermQuery("age", 35))) + .query(); + assertEquals(expectedQuery, actualQuery); + } + + @Test + public void testSpanWithinQueryParser() throws IOException { + IndexQueryParserService queryParser = queryParser(); + Query expectedQuery = new SpanWithinQuery(new SpanTermQuery(new Term("age", longToPrefixCoded(34, 0))), + new SpanTermQuery(new Term("age", longToPrefixCoded(35, 0)))); + String queryText = copyToStringFromClasspath("/org/elasticsearch/index/query/spanWithin.json"); + Query actualQuery = queryParser.parse(queryText).query(); + assertEquals(expectedQuery, actualQuery); + } + + @Test + public void testSpanContainingQueryBuilder() throws IOException { + IndexQueryParserService queryParser = queryParser(); + Query expectedQuery = new SpanContainingQuery(new SpanTermQuery(new Term("age", longToPrefixCoded(34, 0))), + new SpanTermQuery(new Term("age", longToPrefixCoded(35, 0)))); + Query actualQuery = queryParser.parse(spanContainingQuery() + .big(spanTermQuery("age", 34)) + .little(spanTermQuery("age", 35))) + .query(); + assertEquals(expectedQuery, actualQuery); + } + + @Test + public void testSpanContainingQueryParser() throws IOException { + IndexQueryParserService queryParser = queryParser(); + Query expectedQuery = new SpanContainingQuery(new SpanTermQuery(new Term("age", longToPrefixCoded(34, 0))), + new SpanTermQuery(new Term("age", longToPrefixCoded(35, 0)))); + String queryText = copyToStringFromClasspath("/org/elasticsearch/index/query/spanContaining.json"); + Query actualQuery = queryParser.parse(queryText).query(); + assertEquals(expectedQuery, actualQuery); + } + @Test public void testSpanFirstQueryBuilder() throws IOException { IndexQueryParserService queryParser = queryParser(); diff --git a/src/test/java/org/elasticsearch/index/query/spanContaining.json b/src/test/java/org/elasticsearch/index/query/spanContaining.json new file mode 100644 index 00000000000..13f91d88b44 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/spanContaining.json @@ -0,0 +1,14 @@ +{ + span_containing:{ + big:{ + span_term:{ + age:34 + } + }, + little:{ + span_term:{ + age:35 + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/index/query/spanWithin.json b/src/test/java/org/elasticsearch/index/query/spanWithin.json new file mode 100644 index 00000000000..7cf767cdf12 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/spanWithin.json @@ -0,0 +1,14 @@ +{ + span_within:{ + big:{ + span_term:{ + age:34 + } + }, + little:{ + span_term:{ + age:35 + } + } + } +} \ No newline at end of file