From e30aa6b22178c32f6bfe8dfa0d5d99e8a99a9833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Ha=CC=88gerstrand?= Date: Wed, 30 Jan 2013 21:42:48 -0800 Subject: [PATCH] Support SpanMultiTerm, closes #2610, #2400 This adds support for lucene span multi term queries. This lucene query allows users to form complicated queries such as wildcards or prefix queries embedded within span queries. --- .../index/query/FuzzyQueryBuilder.java | 2 +- .../index/query/MultiTermQueryBuilder.java | 5 ++ .../index/query/PrefixQueryBuilder.java | 2 +- .../index/query/QueryBuilders.java | 14 ++++ .../index/query/RangeQueryBuilder.java | 2 +- .../query/SpanMultiTermQueryBuilder.java | 24 ++++++ .../index/query/SpanMultiTermQueryParser.java | 82 +++++++++++++++++++ .../index/query/WildcardQueryBuilder.java | 2 +- .../indices/query/IndicesQueriesModule.java | 2 +- .../query/SimpleIndexQueryParserTests.java | 54 ++++++++++++ .../query/span-multi-term-fuzzy-range.json | 13 +++ .../query/span-multi-term-fuzzy-term.json | 12 +++ .../index/query/span-multi-term-prefix.json | 7 ++ .../query/span-multi-term-range-numeric.json | 16 ++++ .../query/span-multi-term-range-term.json | 16 ++++ .../index/query/span-multi-term-wildcard.json | 7 ++ 16 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/elasticsearch/index/query/MultiTermQueryBuilder.java create mode 100644 src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryBuilder.java create mode 100644 src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java create mode 100644 src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-range.json create mode 100644 src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-term.json create mode 100644 src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-prefix.json create mode 100644 src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-numeric.json create mode 100644 src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-term.json create mode 100644 src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-wildcard.json diff --git a/src/main/java/org/elasticsearch/index/query/FuzzyQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/FuzzyQueryBuilder.java index 6c94e9ea82f..124a509f6fa 100644 --- a/src/main/java/org/elasticsearch/index/query/FuzzyQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/FuzzyQueryBuilder.java @@ -28,7 +28,7 @@ import java.io.IOException; * * */ -public class FuzzyQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { +public class FuzzyQueryBuilder extends BaseQueryBuilder implements MultiTermQueryBuilder, BoostableQueryBuilder { private final String name; diff --git a/src/main/java/org/elasticsearch/index/query/MultiTermQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/MultiTermQueryBuilder.java new file mode 100644 index 00000000000..7ca07889087 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/MultiTermQueryBuilder.java @@ -0,0 +1,5 @@ +package org.elasticsearch.index.query; + +public interface MultiTermQueryBuilder extends QueryBuilder{ + +} diff --git a/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java index 2e5e80aed42..0dd8c521bfc 100644 --- a/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/PrefixQueryBuilder.java @@ -28,7 +28,7 @@ import java.io.IOException; * * */ -public class PrefixQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { +public class PrefixQueryBuilder extends BaseQueryBuilder implements MultiTermQueryBuilder, BoostableQueryBuilder { private final String name; diff --git a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java index fc259f0c612..6ae65d7ce8f 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java @@ -440,6 +440,20 @@ public abstract class QueryBuilders { public static SpanOrQueryBuilder spanOrQuery() { return new SpanOrQueryBuilder(); } + + /** + * Creates a {@link SpanQueryBuilder} which allows having a sub query + * which implements {@link MultiTermQueryBuilder}. This is useful for + * having e.g. wildcard or fuzzy queries inside spans. + * + * @param multiTermQueryBuilder The {@link MultiTermQueryBuilder} that + * backs the created builder. + * @return + */ + + public static SpanMultiTermQueryBuilder spanMultiTermQueryBuilder(MultiTermQueryBuilder multiTermQueryBuilder){ + return new SpanMultiTermQueryBuilder(multiTermQueryBuilder); + } public static FieldMaskingSpanQueryBuilder fieldMaskingSpanQuery(SpanQueryBuilder query, String field) { return new FieldMaskingSpanQueryBuilder(query, field); diff --git a/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java index fd2fbc10e86..fbffd45eccc 100644 --- a/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java @@ -28,7 +28,7 @@ import java.io.IOException; * * */ -public class RangeQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { +public class RangeQueryBuilder extends BaseQueryBuilder implements MultiTermQueryBuilder, BoostableQueryBuilder { private final String name; diff --git a/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryBuilder.java new file mode 100644 index 00000000000..dc950e1e141 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryBuilder.java @@ -0,0 +1,24 @@ +package org.elasticsearch.index.query; + +import java.io.IOException; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +public class SpanMultiTermQueryBuilder extends BaseQueryBuilder implements SpanQueryBuilder{ + + private MultiTermQueryBuilder multiTermQueryBuilder; + + public SpanMultiTermQueryBuilder(MultiTermQueryBuilder multiTermQueryBuilder) { + this.multiTermQueryBuilder = multiTermQueryBuilder; + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) + throws IOException { + builder.startObject(SpanMultiTermQueryParser.NAME); + builder.field(SpanMultiTermQueryParser.MATCH_NAME); + multiTermQueryBuilder.toXContent(builder, params); + builder.endObject(); + } + +} diff --git a/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java b/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java new file mode 100644 index 00000000000..bb4c5049a5f --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/SpanMultiTermQueryParser.java @@ -0,0 +1,82 @@ +package org.elasticsearch.index.query; + +import java.io.IOException; + +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; + +public class SpanMultiTermQueryParser implements QueryParser { + + public static final String NAME = "span_multi_term"; + public static final String MATCH_NAME = "match"; + + @Inject + public SpanMultiTermQueryParser() { + } + + @Override + public String[] names() { + return new String[] { NAME, Strings.toCamelCase(NAME) }; + } + + @Override + @Nullable + public Query parse(QueryParseContext parseContext) throws IOException, + QueryParsingException { + XContentParser parser = parseContext.parser(); + Token token = parser.nextToken(); + checkCorrectField(parseContext, parser, token); + token = parser.nextToken(); + checkHasObject(parseContext, parser.currentToken()); + Query ret = new SpanMultiTermQueryWrapper(getSubQuery( + parseContext, parser)); + parser.nextToken(); + return ret; + } + + + private void checkCorrectField(QueryParseContext parseContext, + XContentParser parser, Token token) throws IOException { + if (!MATCH_NAME.equals(parser.currentName()) + || token != XContentParser.Token.FIELD_NAME) { + throwInvalidClause(parseContext); + } + } + + private MultiTermQuery getSubQuery(QueryParseContext parseContext, + XContentParser parser) throws IOException { + return tryParseSubQuery(parseContext, parser); + } + + private MultiTermQuery tryParseSubQuery(QueryParseContext parseContext, + XContentParser parser) throws IOException { + Query subQuery = parseContext.parseInnerQuery(); + if (!(subQuery instanceof MultiTermQuery)) { + throwInvalidSub(parseContext); + } + return (MultiTermQuery) subQuery; + } + + private void throwInvalidSub(QueryParseContext parseContext) { + throw new QueryParsingException(parseContext.index(), "spanMultiTerm [" + + MATCH_NAME + "] must be of type multi term query"); + } + + private void checkHasObject(QueryParseContext parseContext, Token token) { + if (token != XContentParser.Token.START_OBJECT) { + throwInvalidClause(parseContext); + } + } + + private void throwInvalidClause(QueryParseContext parseContext) { + throw new QueryParsingException(parseContext.index(), + "spanMultiTerm must have [" + MATCH_NAME + + "] multi term query clause"); + } +} diff --git a/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java index 27ef5bff001..0205b542afa 100644 --- a/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/WildcardQueryBuilder.java @@ -33,7 +33,7 @@ import java.io.IOException; * * */ -public class WildcardQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { +public class WildcardQueryBuilder extends BaseQueryBuilder implements MultiTermQueryBuilder, BoostableQueryBuilder { private final String name; diff --git a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java index c5d1a6bae98..9c5ea92634b 100644 --- a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java +++ b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java @@ -102,7 +102,7 @@ public class IndicesQueriesModule extends AbstractModule { qpBinders.addBinding().to(WrapperQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(IndicesQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(CommonTermsQueryParser.class).asEagerSingleton(); - + qpBinders.addBinding().to(SpanMultiTermQueryParser.class).asEagerSingleton(); if (ShapesAvailability.JTS_AVAILABLE) { qpBinders.addBinding().to(GeoShapeQueryParser.class).asEagerSingleton(); } diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java b/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java index d4125c51e3f..192eda9a7b5 100644 --- a/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java +++ b/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java @@ -1539,6 +1539,60 @@ public class SimpleIndexQueryParserTests { assertThat(((SpanTermQuery) spanOrQuery.getClauses()[2]).getTerm(), equalTo(new Term("age", longToPrefixCoded(36, 0)))); } + @Test + public void testSpanMultiTermWildcardQuery() throws IOException { + WildcardQuery expectedWrapped = new WildcardQuery(new Term("user", "ki*y")); + expectedWrapped.setBoost(1.08f); + testSpanMultiTerm("/org/elasticsearch/test/unit/index/query/span-multi-term-wildcard.json", expectedWrapped); + } + + @Test + public void testSpanMultiTermPrefixQuery() throws IOException { + PrefixQuery expectedWrapped = new PrefixQuery(new Term("user", "ki")); + expectedWrapped.setBoost(1.08f); + testSpanMultiTerm("/org/elasticsearch/test/unit/index/query/span-multi-term-prefix.json", expectedWrapped); + } + + @Test + public void testSpanMultiTermFuzzyTermQuery() throws IOException { + IndexQueryParserService queryParser = queryParser(); + String query = copyToStringFromClasspath("/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-term.json"); + Query parsedQuery = queryParser.parse(query).query(); + assertThat(parsedQuery, instanceOf(SpanMultiTermQueryWrapper.class)); + SpanMultiTermQueryWrapper wrapper = (SpanMultiTermQueryWrapper) parsedQuery; + assertThat(wrapper.getField(), equalTo("user")); + } + + @Test + public void testSpanMultiTermFuzzyRangeQuery() throws IOException { + NumericRangeQuery expectedWrapped = NumericRangeQuery.newLongRange("age", 7l, 17l, true, true); + expectedWrapped.setBoost(2.0f); + testSpanMultiTerm("/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-range.json", expectedWrapped); + } + + @Test + public void testSpanMultiTermNumericRangeQuery() throws IOException { + NumericRangeQuery expectedWrapped = NumericRangeQuery.newLongRange("age", 10l, 20l, true, false); + expectedWrapped.setBoost(2.0f); + testSpanMultiTerm("/org/elasticsearch/test/unit/index/query/span-multi-term-range-numeric.json", expectedWrapped); + } + @Test + + public void testSpanMultiTermTermRangeQuery() throws IOException { + NumericRangeQuery expectedWrapped = NumericRangeQuery.newLongRange("age", 10l, 20l, true, false); + expectedWrapped.setBoost(2.0f); + testSpanMultiTerm("/org/elasticsearch/test/unit/index/query/span-multi-term-range-numeric.json", expectedWrapped); + } + + private void testSpanMultiTerm(String file, MultiTermQuery expectedWrapped) throws IOException{ + IndexQueryParserService queryParser = queryParser(); + String query = copyToStringFromClasspath(file); + Query parsedQuery = queryParser.parse(query).query(); + assertThat(parsedQuery, instanceOf(SpanMultiTermQueryWrapper.class)); + SpanMultiTermQueryWrapper wrapper = (SpanMultiTermQueryWrapper) parsedQuery; + assertThat(wrapper, equalTo(new SpanMultiTermQueryWrapper(expectedWrapped))); + } + @Test public void testQueryFilterBuilder() throws Exception { IndexQueryParserService queryParser = queryParser(); diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-range.json b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-range.json new file mode 100644 index 00000000000..f074190a3f6 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-range.json @@ -0,0 +1,13 @@ +{ + "span_multi_term":{ + "match":{ + "fuzzy":{ + "age":{ + "value":12, + "min_similarity":5, + "boost":2.0 + } + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-term.json b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-term.json new file mode 100644 index 00000000000..9511f2212c9 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-fuzzy-term.json @@ -0,0 +1,12 @@ +{ + "span_multi_term":{ + "match":{ + "fuzzy" : { + "user" : { + "value" : "ki", + "boost" : 1.08 + } + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-prefix.json b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-prefix.json new file mode 100644 index 00000000000..dd802cc4687 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-prefix.json @@ -0,0 +1,7 @@ +{ + "span_multi_term":{ + "match":{ + "prefix" : { "user" : { "value" : "ki", "boost" : 1.08 } } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-numeric.json b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-numeric.json new file mode 100644 index 00000000000..b3350736654 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-numeric.json @@ -0,0 +1,16 @@ +{ + "span_multi_term":{ + "match":{ + "range" : { + "age" : { + "from" : 10, + "to" : 20, + "include_lower" : true, + "include_upper": false, + "boost" : 2.0 + } + } + } + } +} + diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-term.json b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-term.json new file mode 100644 index 00000000000..b3350736654 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-range-term.json @@ -0,0 +1,16 @@ +{ + "span_multi_term":{ + "match":{ + "range" : { + "age" : { + "from" : 10, + "to" : 20, + "include_lower" : true, + "include_upper": false, + "boost" : 2.0 + } + } + } + } +} + diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-wildcard.json b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-wildcard.json new file mode 100644 index 00000000000..a251a0f30b2 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/span-multi-term-wildcard.json @@ -0,0 +1,7 @@ +{ + "span_multi_term":{ + "match":{ + "wildcard" : { "user" : {"value": "ki*y" , "boost" : 1.08}} + } + } +} \ No newline at end of file