From 167d35807c5ce4b8e10228180dcf58b14f3ce826 Mon Sep 17 00:00:00 2001 From: kimchy Date: Thu, 9 Dec 2010 07:25:05 +0200 Subject: [PATCH] Add terms/in query, alias terms filter to be in filter as well, closes #557. --- .../index/query/IndexQueryParserModule.java | 1 + .../index/query/xcontent/FilterBuilders.java | 60 +++++++ .../index/query/xcontent/QueryBuilders.java | 131 ++++++++++++++ .../query/xcontent/TermsFilterParser.java | 2 +- .../query/xcontent/TermsQueryBuilder.java | 163 ++++++++++++++++++ .../query/xcontent/TermsQueryParser.java | 123 +++++++++++++ .../xcontent/SimpleIndexQueryParserTests.java | 17 ++ .../index/query/xcontent/terms-query.json | 3 + 8 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryBuilder.java create mode 100644 modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryParser.java create mode 100644 modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/terms-query.json diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java index d3a5bb2d5a9..9b58f472955 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java @@ -227,6 +227,7 @@ public class IndexQueryParserModule extends AbstractModule { bindings.processXContentQueryParser(QueryStringQueryParser.NAME, QueryStringQueryParser.class); bindings.processXContentQueryParser(BoolQueryParser.NAME, BoolQueryParser.class); bindings.processXContentQueryParser(TermQueryParser.NAME, TermQueryParser.class); + bindings.processXContentQueryParser(TermsQueryParser.NAME, TermsQueryParser.class); bindings.processXContentQueryParser(FuzzyQueryParser.NAME, FuzzyQueryParser.class); bindings.processXContentQueryParser(FieldQueryParser.NAME, FieldQueryParser.class); bindings.processXContentQueryParser(RangeQueryParser.NAME, RangeQueryParser.class); diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java index 95b71b762b7..58010fcd0dc 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/FilterBuilders.java @@ -153,6 +153,66 @@ public abstract class FilterBuilders { return new TermsFilterBuilder(name, values); } + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsFilterBuilder inFilter(String name, String... values) { + return new TermsFilterBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsFilterBuilder inFilter(String name, int... values) { + return new TermsFilterBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsFilterBuilder inFilter(String name, long... values) { + return new TermsFilterBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsFilterBuilder inFilter(String name, float... values) { + return new TermsFilterBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsFilterBuilder inFilter(String name, double... values) { + return new TermsFilterBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsFilterBuilder inFilter(String name, Object... values) { + return new TermsFilterBuilder(name, values); + } + /** * A filter that restricts search results to values that have a matching prefix in a given * field. diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java index ba968930290..718849f592c 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java @@ -422,6 +422,137 @@ public abstract class QueryBuilders { return new HasChildQueryBuilder(type, query); } + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder termsQuery(String name, String... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder termsQuery(String name, int... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder termsQuery(String name, long... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder termsQuery(String name, float... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder termsQuery(String name, double... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder termsQuery(String name, Object... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder inQuery(String name, String... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder inQuery(String name, int... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder inQuery(String name, long... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder inQuery(String name, float... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder inQuery(String name, double... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public static TermsQueryBuilder inQuery(String name, Object... values) { + return new TermsQueryBuilder(name, values); + } + + /** + * A filter that restricts search results to values that have a matching prefix in a given + * field. + * + * @param name The field name + * @param prefix The prefix + */ + public static PrefixFilterBuilder inQuery(String name, String prefix) { + return new PrefixFilterBuilder(name, prefix); + } + private QueryBuilders() { } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsFilterParser.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsFilterParser.java index 58be4819212..82149bfaa26 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsFilterParser.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsFilterParser.java @@ -49,7 +49,7 @@ public class TermsFilterParser extends AbstractIndexComponent implements XConten } @Override public String[] names() { - return new String[]{NAME}; + return new String[]{NAME, "in"}; } @Override public Filter parse(QueryParseContext parseContext) throws IOException, QueryParsingException { diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryBuilder.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryBuilder.java new file mode 100644 index 00000000000..1061e4b5956 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryBuilder.java @@ -0,0 +1,163 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.xcontent; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * @author kimchy (shay.banon) + */ +public class TermsQueryBuilder extends BaseQueryBuilder { + + private final String name; + + private final Object[] values; + + private int minimumMatch = -1; + + private Boolean disableCoord; + + private float boost = -1; + + /** + * A query for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public TermsQueryBuilder(String name, String... values) { + this(name, (Object[]) values); + } + + /** + * A query for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public TermsQueryBuilder(String name, int... values) { + this.name = name; + this.values = new Integer[values.length]; + for (int i = 0; i < values.length; i++) { + this.values[i] = values[i]; + } + } + + /** + * A query for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public TermsQueryBuilder(String name, long... values) { + this.name = name; + this.values = new Long[values.length]; + for (int i = 0; i < values.length; i++) { + this.values[i] = values[i]; + } + } + + /** + * A query for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public TermsQueryBuilder(String name, float... values) { + this.name = name; + this.values = new Float[values.length]; + for (int i = 0; i < values.length; i++) { + this.values[i] = values[i]; + } + } + + /** + * A query for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public TermsQueryBuilder(String name, double... values) { + this.name = name; + this.values = new Double[values.length]; + for (int i = 0; i < values.length; i++) { + this.values[i] = values[i]; + } + } + + /** + * A query for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + public TermsQueryBuilder(String name, Object... values) { + this.name = name; + this.values = values; + } + + /** + * Sets the minimum number of matches across the provided terms. Defaults to 1. + */ + public TermsQueryBuilder minimumMatch(int minimumMatch) { + this.minimumMatch = minimumMatch; + return this; + } + + /** + * Sets the boost for this query. Documents matching this query will (in addition to the normal + * weightings) have their score multiplied by the boost provided. + */ + public TermsQueryBuilder boost(float boost) { + this.boost = boost; + return this; + } + + /** + * Disables Similarity#coord(int,int) in scoring. Defualts to false. + */ + public TermsQueryBuilder disableCoord(boolean disableCoord) { + this.disableCoord = disableCoord; + return this; + } + + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(TermsQueryParser.NAME); + builder.startArray(name); + for (Object value : values) { + builder.value(value); + } + builder.endArray(); + + if (minimumMatch != -1) { + builder.field("minimum_match", minimumMatch); + } + if (disableCoord != null) { + builder.field("disable_coord", disableCoord); + } + if (boost != -1) { + builder.field("boost", boost); + } + + builder.endObject(); + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryParser.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryParser.java new file mode 100644 index 00000000000..bd3b6b6fd32 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/TermsQueryParser.java @@ -0,0 +1,123 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.xcontent; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.AbstractIndexComponent; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.query.QueryParsingException; +import org.elasticsearch.index.settings.IndexSettings; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.common.collect.Lists.*; +import static org.elasticsearch.common.lucene.search.Queries.*; +import static org.elasticsearch.index.query.support.QueryParsers.*; + +/** + *
+ * "terms" : {
+ *  "field_name" : [ "value1", "value2" ]
+ *  "minimum_match" : 1
+ * }
+ * 
+ * + * @author kimchy (shay.banon) + */ +public class TermsQueryParser extends AbstractIndexComponent implements XContentQueryParser { + + public static final String NAME = "terms"; + + @Inject public TermsQueryParser(Index index, @IndexSettings Settings settings) { + super(index, settings); + } + + @Override public String[] names() { + return new String[]{NAME, "in"}; // allow both "in" and "terms" (since its similar to the "terms" filter) + } + + @Override public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + String fieldName = null; + boolean disableCoord = false; + float boost = 1.0f; + int minimumNumberShouldMatch = 1; + List values = newArrayList(); + + 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_ARRAY) { + fieldName = currentFieldName; + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + String value = parser.text(); + if (value == null) { + throw new QueryParsingException(index, "No value specified for terms query"); + } + values.add(value); + } + } else if (token.isValue()) { + if ("disable_coord".equals(currentFieldName) || "disableCoord".equals(currentFieldName)) { + disableCoord = parser.booleanValue(); + } else if ("minimum_match".equals(currentFieldName) || "minimumMatch".equals(currentFieldName)) { + minimumNumberShouldMatch = parser.intValue(); + } else if ("boost".equals(currentFieldName)) { + boost = parser.floatValue(); + } + } + } + + FieldMapper mapper = null; + MapperService.SmartNameFieldMappers smartNameFieldMappers = parseContext.smartFieldMappers(fieldName); + if (smartNameFieldMappers != null) { + if (smartNameFieldMappers.hasMapper()) { + mapper = smartNameFieldMappers.mapper(); + } + } + + BooleanQuery query = new BooleanQuery(disableCoord); + for (String value : values) { + if (mapper != null) { + query.add(new BooleanClause(mapper.fieldQuery(value), BooleanClause.Occur.SHOULD)); + } else { + query.add(new TermQuery(new Term(fieldName, value)), BooleanClause.Occur.SHOULD); + } + } + query.setBoost(boost); + if (minimumNumberShouldMatch != -1) { + query.setMinimumNumberShouldMatch(minimumNumberShouldMatch); + } + return wrapSmartNameQuery(optimizeQuery(fixNegativeQueryIfNeeded(query)), smartNameFieldMappers, parseContext); + } +} + diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java index 6f98da40437..698d7ad8102 100644 --- a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java @@ -760,6 +760,23 @@ public class SimpleIndexQueryParserTests { assertThat(clauses[3].getOccur(), equalTo(BooleanClause.Occur.SHOULD)); } + @Test public void testTermsQuery() throws IOException { + IndexQueryParser queryParser = queryParser(); + String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/terms-query.json"); + Query parsedQuery = queryParser.parse(query).query(); + assertThat(parsedQuery, instanceOf(BooleanQuery.class)); + BooleanQuery booleanQuery = (BooleanQuery) parsedQuery; + BooleanClause[] clauses = booleanQuery.getClauses(); + + assertThat(clauses.length, equalTo(2)); + + assertThat(((TermQuery) clauses[0].getQuery()).getTerm(), equalTo(new Term("name.first", "shay"))); + assertThat(clauses[0].getOccur(), equalTo(BooleanClause.Occur.SHOULD)); + + assertThat(((TermQuery) clauses[1].getQuery()).getTerm(), equalTo(new Term("name.first", "test"))); + assertThat(clauses[1].getOccur(), equalTo(BooleanClause.Occur.SHOULD)); + } + @Test public void testFilteredQueryBuilder() throws IOException { IndexQueryParser queryParser = queryParser(); Query parsedQuery = queryParser.parse(filteredQuery(termQuery("name.first", "shay"), termFilter("name.last", "banon"))).query(); diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/terms-query.json b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/terms-query.json new file mode 100644 index 00000000000..9a4afe12a1d --- /dev/null +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/terms-query.json @@ -0,0 +1,3 @@ +{ + "terms" : { "name.first" : ["shay", "test"] } +} \ No newline at end of file