Refactors SimpleQueryStringBuilder/Parser

This commit makes SimpleQueryStringBuilder streamable, add hashCode and equals. Adds a dedicated builder/parser unit test, fixes formatting, adds JavaDoc where needed, adjust the handling of default values according to https://github.com/elastic/dev/blob/master/design/queries/general-guidelines.md

Switched to using toLanguageTag/forLanguageTag when parsing Locales. Using LocaleUtils from either Elasticsearch or Apache commons resulted in Locales not passing the roundtrip test. For more info see https://issues.apache.org/jira/browse/LUCENE-4021

Relates to #10217
This commit is contained in:
Isabel Drost-Fromm 2015-06-09 09:13:50 +02:00
parent 33b3323a63
commit e170c8e498
5 changed files with 724 additions and 154 deletions

View File

@ -29,6 +29,7 @@ import org.apache.lucene.util.BytesRef;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/**
* Wrapper class for Lucene's SimpleQueryParser that allows us to redefine
@ -202,51 +203,102 @@ public class SimpleQueryParser extends org.apache.lucene.queryparser.simple.Simp
return new PrefixQuery(new Term(field, termStr));
}
}
/**
* Class encapsulating the settings for the SimpleQueryString query, with
* their default values
*/
public static class Settings {
private Locale locale = Locale.ROOT;
private boolean lowercaseExpandedTerms = true;
private boolean lenient = false;
private boolean analyzeWildcard = false;
static class Settings {
/** Locale to use for parsing. */
private Locale locale = SimpleQueryStringBuilder.DEFAULT_LOCALE;
/** Specifies whether parsed terms should be lowercased. */
private boolean lowercaseExpandedTerms = SimpleQueryStringBuilder.DEFAULT_LOWERCASE_EXPANDED_TERMS;
/** Specifies whether lenient query parsing should be used. */
private boolean lenient = SimpleQueryStringBuilder.DEFAULT_LENIENT;
/** Specifies whether wildcards should be analyzed. */
private boolean analyzeWildcard = SimpleQueryStringBuilder.DEFAULT_ANALYZE_WILDCARD;
/**
* Generates default {@link Settings} object (uses ROOT locale, does
* lowercase terms, no lenient parsing, no wildcard analysis).
* */
public Settings() {
}
public void locale(Locale locale) {
public Settings(Locale locale, Boolean lowercaseExpandedTerms, Boolean lenient, Boolean analyzeWildcard) {
this.locale = locale;
this.lowercaseExpandedTerms = lowercaseExpandedTerms;
this.lenient = lenient;
this.analyzeWildcard = analyzeWildcard;
}
/** Specifies the locale to use for parsing, Locale.ROOT by default. */
public void locale(Locale locale) {
this.locale = (locale != null) ? locale : SimpleQueryStringBuilder.DEFAULT_LOCALE;
}
/** Returns the locale to use for parsing. */
public Locale locale() {
return this.locale;
}
/**
* Specifies whether to lowercase parse terms, defaults to true if
* unset.
*/
public void lowercaseExpandedTerms(boolean lowercaseExpandedTerms) {
this.lowercaseExpandedTerms = lowercaseExpandedTerms;
}
/** Returns whether to lowercase parse terms. */
public boolean lowercaseExpandedTerms() {
return this.lowercaseExpandedTerms;
}
/** Specifies whether to use lenient parsing, defaults to false. */
public void lenient(boolean lenient) {
this.lenient = lenient;
}
/** Returns whether to use lenient parsing. */
public boolean lenient() {
return this.lenient;
}
/** Specifies whether to analyze wildcards. Defaults to false if unset. */
public void analyzeWildcard(boolean analyzeWildcard) {
this.analyzeWildcard = analyzeWildcard;
}
/** Returns whether to analyze wildcards. */
public boolean analyzeWildcard() {
return analyzeWildcard;
}
@Override
public int hashCode() {
// checking the return value of toLanguageTag() for locales only.
// For further reasoning see
// https://issues.apache.org/jira/browse/LUCENE-4021
return Objects.hash(locale.toLanguageTag(), lowercaseExpandedTerms, lenient, analyzeWildcard);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Settings other = (Settings) obj;
// checking the return value of toLanguageTag() for locales only.
// For further reasoning see
// https://issues.apache.org/jira/browse/LUCENE-4021
return (Objects.equals(locale.toLanguageTag(), other.locale.toLanguageTag())
&& Objects.equals(lowercaseExpandedTerms, other.lowercaseExpandedTerms)
&& Objects.equals(lenient, other.lenient)
&& Objects.equals(analyzeWildcard, other.analyzeWildcard));
}
}
}

View File

@ -19,151 +19,352 @@
package org.elasticsearch.index.query;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.BooleanClause.Occur;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.query.SimpleQueryParser.Settings;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
/**
* SimpleQuery is a query parser that acts similar to a query_string
* query, but won't throw exceptions for any weird string syntax.
* SimpleQuery is a query parser that acts similar to a query_string query, but
* won't throw exceptions for any weird string syntax.
*
* For more detailed explanation of the query string syntax see also the <a
* href=
* "https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html"
* > online documentation</a>.
*/
public class SimpleQueryStringBuilder extends AbstractQueryBuilder<SimpleQueryStringBuilder> implements BoostableQueryBuilder<SimpleQueryStringBuilder> {
/** Default locale used for parsing.*/
public static final Locale DEFAULT_LOCALE = Locale.ROOT;
/** Default for lowercasing parsed terms.*/
public static final boolean DEFAULT_LOWERCASE_EXPANDED_TERMS = true;
/** Default for using lenient query parsing.*/
public static final boolean DEFAULT_LENIENT = false;
/** Default for wildcard analysis.*/
public static final boolean DEFAULT_ANALYZE_WILDCARD = false;
/** Default for boost to apply to resulting Lucene query. Defaults to 1.0*/
public static final float DEFAULT_BOOST = 1.0f;
/** Default for default operator to use for linking boolean clauses.*/
public static final Operator DEFAULT_OPERATOR = Operator.OR;
/** Default for search flags to use. */
public static final int DEFAULT_FLAGS = SimpleQueryStringFlag.ALL.value;
/** Name for (de-)serialization. */
public static final String NAME = "simple_query_string";
private Map<String, Float> fields = new HashMap<>();
private String analyzer;
private Operator operator;
/** Query text to parse. */
private final String queryText;
/** Boost to apply to resulting Lucene query. Defaults to 1.0*/
private float boost = DEFAULT_BOOST;
/**
* Fields to query against. If left empty will query default field,
* currently _ALL. Uses a TreeMap to hold the fields so boolean clauses are
* always sorted in same order for generated Lucene query for easier
* testing.
*
* Can be changed back to HashMap once https://issues.apache.org/jira/browse/LUCENE-6305 is fixed.
*/
private final Map<String, Float> fieldsAndWeights = new TreeMap<>();
/** If specified, analyzer to use to parse the query text, defaults to registered default in toQuery. */
private String analyzer;
/** Name of the query. Optional.*/
private String queryName;
/** Default operator to use for linking boolean clauses. Defaults to OR according to docs. */
private Operator defaultOperator = DEFAULT_OPERATOR;
/** If result is a boolean query, minimumShouldMatch parameter to apply. Ignored otherwise. */
private String minimumShouldMatch;
private int flags = -1;
private float boost = -1.0f;
private Boolean lowercaseExpandedTerms;
private Boolean lenient;
private Boolean analyzeWildcard;
private Locale locale;
/** Any search flags to be used, ALL by default. */
private int flags = DEFAULT_FLAGS;
/** Further search settings needed by the ES specific query string parser only. */
private Settings settings = new Settings();
static final SimpleQueryStringBuilder PROTOTYPE = new SimpleQueryStringBuilder(null);
/**
* Operators for the default_operator
*/
/** Operators available for linking boolean clauses. */
// Move out after #11345 is in.
public static enum Operator {
AND,
OR
OR;
public static Operator parseFromInt(int ordinal) {
switch (ordinal) {
case 0:
return AND;
case 1:
return OR;
default:
throw new IllegalArgumentException("cannot parse Operator from ordinal " + ordinal);
}
}
}
/**
* Construct a new simple query with the given text
*/
public SimpleQueryStringBuilder(String text) {
this.queryText = text;
/** Construct a new simple query with this query string. */
public SimpleQueryStringBuilder(String queryText) {
this.queryText = queryText;
}
/** Set the boost of this query. */
@Override
public SimpleQueryStringBuilder boost(float boost) {
this.boost = boost;
return this;
}
/** Returns the boost of this query. */
/** Returns the boost to apply to resulting Lucene query.*/
public float boost() {
return this.boost;
}
/** Returns the text to parse the query from. */
public String text() {
return this.queryText;
}
/**
* Add a field to run the query against
*/
/** Add a field to run the query against. */
public SimpleQueryStringBuilder field(String field) {
this.fields.put(field, null);
if (Strings.isEmpty(field)) {
throw new IllegalArgumentException("supplied field is null or empty.");
}
this.fieldsAndWeights.put(field, 1.0f);
return this;
}
/**
* Add a field to run the query against with a specific boost
*/
/** Add a field to run the query against with a specific boost. */
public SimpleQueryStringBuilder field(String field, float boost) {
this.fields.put(field, boost);
if (Strings.isEmpty(field)) {
throw new IllegalArgumentException("supplied field is null or empty.");
}
this.fieldsAndWeights.put(field, boost);
return this;
}
/**
* Specify a name for the query
*/
public SimpleQueryStringBuilder queryName(String name) {
this.queryName = name;
/** Add several fields to run the query against with a specific boost. */
public SimpleQueryStringBuilder fields(Map<String, Float> fields) {
this.fieldsAndWeights.putAll(fields);
return this;
}
/**
* Specify an analyzer to use for the query
*/
/** Returns the fields including their respective boosts to run the query against. */
public Map<String, Float> fields() {
return this.fieldsAndWeights;
}
/** Specify an analyzer to use for the query. */
public SimpleQueryStringBuilder analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/**
* Specify the default operator for the query. Defaults to "OR" if no
* operator is specified
*/
public SimpleQueryStringBuilder defaultOperator(Operator defaultOperator) {
this.operator = defaultOperator;
return this;
/** Returns the analyzer to use for the query. */
public String analyzer() {
return this.analyzer;
}
/**
* Specify the enabled features of the SimpleQueryString.
* Specify the default operator for the query. Defaults to "OR" if no
* operator is specified.
*/
public SimpleQueryStringBuilder defaultOperator(Operator defaultOperator) {
this.defaultOperator = (defaultOperator != null) ? defaultOperator : DEFAULT_OPERATOR;
return this;
}
/** Returns the default operator for the query. */
public Operator defaultOperator() {
return this.defaultOperator;
}
/**
* Specify the enabled features of the SimpleQueryString. Defaults to ALL if
* none are specified.
*/
public SimpleQueryStringBuilder flags(SimpleQueryStringFlag... flags) {
int value = 0;
if (flags.length == 0) {
value = SimpleQueryStringFlag.ALL.value;
} else {
if (flags != null && flags.length > 0) {
int value = 0;
for (SimpleQueryStringFlag flag : flags) {
value |= flag.value;
}
this.flags = value;
} else {
this.flags = DEFAULT_FLAGS;
}
this.flags = value;
return this;
}
/** For testing and serialisation only. */
SimpleQueryStringBuilder flags(int flags) {
this.flags = flags;
return this;
}
/** For testing only: Return the flags set for this query. */
int flags() {
return this.flags;
}
/** Set the name for this query. */
public SimpleQueryStringBuilder queryName(String queryName) {
this.queryName = queryName;
return this;
}
/** Returns the name for this query. */
public String queryName() {
return queryName;
}
/**
* Specifies whether parsed terms for this query should be lower-cased.
* Defaults to true if not set.
*/
public SimpleQueryStringBuilder lowercaseExpandedTerms(boolean lowercaseExpandedTerms) {
this.lowercaseExpandedTerms = lowercaseExpandedTerms;
this.settings.lowercaseExpandedTerms(lowercaseExpandedTerms);
return this;
}
/** Returns whether parsed terms should be lower cased for this query. */
public boolean lowercaseExpandedTerms() {
return this.settings.lowercaseExpandedTerms();
}
/** Specifies the locale for parsing terms. Defaults to ROOT if none is set. */
public SimpleQueryStringBuilder locale(Locale locale) {
this.locale = locale;
this.settings.locale(locale);
return this;
}
/** Returns the locale for parsing terms for this query. */
public Locale locale() {
return this.settings.locale();
}
/** Specifies whether query parsing should be lenient. Defaults to false. */
public SimpleQueryStringBuilder lenient(boolean lenient) {
this.lenient = lenient;
this.settings.lenient(lenient);
return this;
}
/** Returns whether query parsing should be lenient. */
public boolean lenient() {
return this.settings.lenient();
}
/** Specifies whether wildcards should be analyzed. Defaults to false. */
public SimpleQueryStringBuilder analyzeWildcard(boolean analyzeWildcard) {
this.analyzeWildcard = analyzeWildcard;
this.settings.analyzeWildcard(DEFAULT_ANALYZE_WILDCARD);
return this;
}
/** Returns whether wildcards should by analyzed. */
public boolean analyzeWildcard() {
return this.settings.analyzeWildcard();
}
/**
* Specifies the minimumShouldMatch to apply to the resulting query should
* that be a Boolean query.
*/
public SimpleQueryStringBuilder minimumShouldMatch(String minimumShouldMatch) {
this.minimumShouldMatch = minimumShouldMatch;
return this;
}
/**
* Returns the minimumShouldMatch to apply to the resulting query should
* that be a Boolean query.
*/
public String minimumShouldMatch() {
return minimumShouldMatch;
}
/**
* {@inheritDoc}
*
* Checks that mandatory queryText is neither null nor empty.
* */
@Override
public QueryValidationException validate() {
QueryValidationException validationException = null;
// Query text is required
if (queryText == null) {
validationException = QueryValidationException.addValidationError("[" + SimpleQueryStringBuilder.NAME + "] query text missing",
validationException);
}
return validationException;
}
@Override
public Query toQuery(QueryParseContext parseContext) {
// Use the default field (_all) if no fields specified
if (fieldsAndWeights.isEmpty()) {
String field = parseContext.defaultField();
fieldsAndWeights.put(field, 1.0F);
}
// Use standard analyzer by default if none specified
Analyzer luceneAnalyzer;
if (analyzer == null) {
luceneAnalyzer = parseContext.mapperService().searchAnalyzer();
} else {
luceneAnalyzer = parseContext.analysisService().analyzer(analyzer);
if (luceneAnalyzer == null) {
throw new QueryParsingException(parseContext, "[" + SimpleQueryStringBuilder.NAME + "] analyzer [" + analyzer
+ "] not found");
}
}
SimpleQueryParser sqp = new SimpleQueryParser(luceneAnalyzer, fieldsAndWeights, flags, settings);
if (defaultOperator != null) {
switch (defaultOperator) {
case OR:
sqp.setDefaultOperator(Occur.SHOULD);
break;
case AND:
sqp.setDefaultOperator(Occur.MUST);
break;
}
}
Query query = sqp.parse(queryText);
if (queryName != null) {
parseContext.addNamedQuery(queryName, query);
}
if (minimumShouldMatch != null && query instanceof BooleanQuery) {
Queries.applyMinimumShouldMatch((BooleanQuery) query, minimumShouldMatch);
}
// safety check - https://github.com/elastic/elasticsearch/pull/11696#discussion-diff-32532468
if (query != null) {
query.setBoost(boost);
}
return query;
}
@Override
public void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME);
builder.field("query", queryText);
if (fields.size() > 0) {
if (fieldsAndWeights.size() > 0) {
builder.startArray("fields");
for (Map.Entry<String, Float> entry : fields.entrySet()) {
for (Map.Entry<String, Float> entry : fieldsAndWeights.entrySet()) {
String field = entry.getKey();
Float boost = entry.getValue();
if (boost != null) {
@ -175,33 +376,16 @@ public class SimpleQueryStringBuilder extends AbstractQueryBuilder<SimpleQuerySt
builder.endArray();
}
if (flags != -1) {
builder.field("flags", flags);
}
if (analyzer != null) {
builder.field("analyzer", analyzer);
}
if (operator != null) {
builder.field("default_operator", operator.name().toLowerCase(Locale.ROOT));
}
if (lowercaseExpandedTerms != null) {
builder.field("lowercase_expanded_terms", lowercaseExpandedTerms);
}
if (lenient != null) {
builder.field("lenient", lenient);
}
if (analyzeWildcard != null) {
builder.field("analyze_wildcard", analyzeWildcard);
}
if (locale != null) {
builder.field("locale", locale.toString());
}
builder.field("flags", flags);
builder.field("default_operator", defaultOperator.name().toLowerCase(Locale.ROOT));
builder.field("lowercase_expanded_terms", settings.lowercaseExpandedTerms());
builder.field("lenient", settings.lenient());
builder.field("analyze_wildcard", settings.analyzeWildcard());
builder.field("locale", (settings.locale().toLanguageTag()));
if (queryName != null) {
builder.field("_name", queryName);
@ -210,7 +394,7 @@ public class SimpleQueryStringBuilder extends AbstractQueryBuilder<SimpleQuerySt
if (minimumShouldMatch != null) {
builder.field("minimum_should_match", minimumShouldMatch);
}
if (boost != -1.0f) {
builder.field("boost", boost);
}
@ -222,4 +406,76 @@ public class SimpleQueryStringBuilder extends AbstractQueryBuilder<SimpleQuerySt
public String getName() {
return NAME;
}
@Override
public SimpleQueryStringBuilder readFrom(StreamInput in) throws IOException {
SimpleQueryStringBuilder result = new SimpleQueryStringBuilder(in.readString());
result.boost = in.readFloat();
int size = in.readInt();
Map<String, Float> fields = new HashMap<>();
for (int i = 0; i < size; i++) {
String field = in.readString();
Float weight = in.readFloat();
fields.put(field, weight);
}
result.fieldsAndWeights.putAll(fields);
result.flags = in.readInt();
result.analyzer = in.readOptionalString();
result.defaultOperator = Operator.parseFromInt(in.readInt());
result.settings.lowercaseExpandedTerms(in.readBoolean());
result.settings.lenient(in.readBoolean());
result.settings.analyzeWildcard(in.readBoolean());
String localeStr = in.readString();
result.settings.locale(Locale.forLanguageTag(localeStr));
result.queryName = in.readOptionalString();
result.minimumShouldMatch = in.readOptionalString();
return result;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(queryText);
out.writeFloat(boost);
out.writeInt(fieldsAndWeights.size());
for (Map.Entry<String, Float> entry : fieldsAndWeights.entrySet()) {
out.writeString(entry.getKey());
out.writeFloat(entry.getValue());
}
out.writeInt(flags);
out.writeOptionalString(analyzer);
out.writeInt(defaultOperator.ordinal());
out.writeBoolean(settings.lowercaseExpandedTerms());
out.writeBoolean(settings.lenient());
out.writeBoolean(settings.analyzeWildcard());
out.writeString(settings.locale().toLanguageTag());
out.writeOptionalString(queryName);
out.writeOptionalString(minimumShouldMatch);
}
@Override
public int hashCode() {
return Objects.hash(fieldsAndWeights, analyzer, defaultOperator, queryText, queryName, minimumShouldMatch, settings);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SimpleQueryStringBuilder other = (SimpleQueryStringBuilder) obj;
return Objects.equals(fieldsAndWeights, other.fieldsAndWeights) && Objects.equals(analyzer, other.analyzer)
&& Objects.equals(defaultOperator, other.defaultOperator) && Objects.equals(queryText, other.queryText)
&& Objects.equals(queryName, other.queryName) && Objects.equals(minimumShouldMatch, other.minimumShouldMatch)
&& Objects.equals(settings, other.settings);
}
}

View File

@ -19,22 +19,15 @@
package org.elasticsearch.index.query;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.LocaleUtils;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.mapper.FieldMapper;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.query.SimpleQueryStringBuilder.Operator;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@ -70,7 +63,7 @@ import java.util.Map;
* {@code fields} - fields to search, defaults to _all if not set, allows
* boosting a field with ^n
*/
public class SimpleQueryStringParser extends BaseQueryParserTemp {
public class SimpleQueryStringParser extends BaseQueryParser {
@Inject
public SimpleQueryStringParser(Settings settings) {
@ -83,7 +76,7 @@ public class SimpleQueryStringParser extends BaseQueryParserTemp {
}
@Override
public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
public QueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException {
XContentParser parser = parseContext.parser();
String currentFieldName = null;
@ -92,11 +85,14 @@ public class SimpleQueryStringParser extends BaseQueryParserTemp {
String queryName = null;
String field = null;
String minimumShouldMatch = null;
Map<String, Float> fieldsAndWeights = null;
BooleanClause.Occur defaultOperator = null;
Analyzer analyzer = null;
Map<String, Float> fieldsAndWeights = new HashMap<>();
Operator defaultOperator = null;
String analyzerName = null;
int flags = -1;
SimpleQueryParser.Settings sqsSettings = new SimpleQueryParser.Settings();
boolean lenient = SimpleQueryStringBuilder.DEFAULT_LENIENT;
boolean lowercaseExpandedTerms = SimpleQueryStringBuilder.DEFAULT_LOWERCASE_EXPANDED_TERMS;
boolean analyzeWildcard = SimpleQueryStringBuilder.DEFAULT_ANALYZE_WILDCARD;
Locale locale = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
@ -121,10 +117,6 @@ public class SimpleQueryStringParser extends BaseQueryParserTemp {
fField = parser.text();
}
if (fieldsAndWeights == null) {
fieldsAndWeights = new HashMap<>();
}
if (Regex.isSimpleMatchPattern(fField)) {
for (String fieldName : parseContext.mapperService().simpleMatchToIndexNames(fField)) {
fieldsAndWeights.put(fieldName, fBoost);
@ -149,18 +141,15 @@ public class SimpleQueryStringParser extends BaseQueryParserTemp {
} else if ("boost".equals(currentFieldName)) {
boost = parser.floatValue();
} else if ("analyzer".equals(currentFieldName)) {
analyzer = parseContext.analysisService().analyzer(parser.text());
if (analyzer == null) {
throw new QueryParsingException(parseContext, "[" + SimpleQueryStringBuilder.NAME + "] analyzer [" + parser.text() + "] not found");
}
analyzerName = parser.text();
} else if ("field".equals(currentFieldName)) {
field = parser.text();
} else if ("default_operator".equals(currentFieldName) || "defaultOperator".equals(currentFieldName)) {
String op = parser.text();
if ("or".equalsIgnoreCase(op)) {
defaultOperator = BooleanClause.Occur.SHOULD;
defaultOperator = Operator.OR;
} else if ("and".equalsIgnoreCase(op)) {
defaultOperator = BooleanClause.Occur.MUST;
defaultOperator = Operator.AND;
} else {
throw new QueryParsingException(parseContext, "[" + SimpleQueryStringBuilder.NAME + "] default operator [" + op + "] is not allowed");
}
@ -177,14 +166,13 @@ public class SimpleQueryStringParser extends BaseQueryParserTemp {
}
} else if ("locale".equals(currentFieldName)) {
String localeStr = parser.text();
Locale locale = LocaleUtils.parse(localeStr);
sqsSettings.locale(locale);
locale = Locale.forLanguageTag(localeStr);
} else if ("lowercase_expanded_terms".equals(currentFieldName)) {
sqsSettings.lowercaseExpandedTerms(parser.booleanValue());
lowercaseExpandedTerms = parser.booleanValue();
} else if ("lenient".equals(currentFieldName)) {
sqsSettings.lenient(parser.booleanValue());
lenient = parser.booleanValue();
} else if ("analyze_wildcard".equals(currentFieldName)) {
sqsSettings.analyzeWildcard(parser.booleanValue());
analyzeWildcard = parser.booleanValue();
} else if ("_name".equals(currentFieldName)) {
queryName = parser.text();
} else if ("minimum_should_match".equals(currentFieldName)) {
@ -199,45 +187,18 @@ public class SimpleQueryStringParser extends BaseQueryParserTemp {
if (queryBody == null) {
throw new QueryParsingException(parseContext, "[" + SimpleQueryStringBuilder.NAME + "] query text missing");
}
// Support specifying only a field instead of a map
if (field == null) {
field = currentFieldName;
}
// Use the default field (_all) if no fields specified
if (fieldsAndWeights == null) {
field = parseContext.defaultField();
}
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder(queryBody);
qb.boost(boost).fields(fieldsAndWeights).analyzer(analyzerName).queryName(queryName).minimumShouldMatch(minimumShouldMatch);
qb.flags(flags).defaultOperator(defaultOperator).locale(locale).lowercaseExpandedTerms(lowercaseExpandedTerms);
qb.lenient(lenient).analyzeWildcard(analyzeWildcard).boost(boost);
// Use standard analyzer by default
if (analyzer == null) {
analyzer = parseContext.mapperService().searchAnalyzer();
}
if (fieldsAndWeights == null) {
fieldsAndWeights = Collections.singletonMap(field, 1.0F);
}
SimpleQueryParser sqp = new SimpleQueryParser(analyzer, fieldsAndWeights, flags, sqsSettings);
if (defaultOperator != null) {
sqp.setDefaultOperator(defaultOperator);
}
Query query = sqp.parse(queryBody);
if (queryName != null) {
parseContext.addNamedQuery(queryName, query);
}
if (minimumShouldMatch != null && query instanceof BooleanQuery) {
Queries.applyMinimumShouldMatch((BooleanQuery) query, minimumShouldMatch);
}
if (query != null) {
query.setBoost(boost);
}
return query;
return qb;
}
@Override

View File

@ -207,8 +207,8 @@ public abstract class BaseQueryTestCase<QB extends QueryBuilder<QB>> extends Ela
QueryBuilder newQuery = queryParserService.queryParser(testQuery.getName()).fromXContent(context);
assertNotSame(newQuery, testQuery);
assertEquals(newQuery, testQuery);
assertEquals(newQuery.hashCode(), testQuery.hashCode());
assertEquals("Queries should be equal: " + newQuery + " vs. " + testQuery, newQuery, testQuery);
assertEquals("Queries should have equal hashcodes: " + newQuery + " vs. " + testQuery, newQuery.hashCode(), testQuery.hashCode());
}
/**

View File

@ -0,0 +1,301 @@
/*
* 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.analysis.Analyzer;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.BooleanClause.Occur;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.index.query.SimpleQueryParser.Settings;
import org.elasticsearch.index.query.SimpleQueryStringBuilder.Operator;
import org.junit.Test;
import java.io.IOException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import static org.hamcrest.Matchers.*;
public class SimpleQueryStringBuilderTest extends BaseQueryTestCase<SimpleQueryStringBuilder> {
private static final String[] MINIMUM_SHOULD_MATCH = new String[] { "1", "-1", "75%", "-25%", "2<75%", "2<-25%" };
@Override
protected SimpleQueryStringBuilder createTestQueryBuilder() {
SimpleQueryStringBuilder result = new SimpleQueryStringBuilder(randomAsciiOfLengthBetween(1, 10));
if (randomBoolean()) {
result.queryName(randomAsciiOfLengthBetween(1, 10));
}
if (randomBoolean()) {
result.analyzeWildcard(randomBoolean());
}
if (randomBoolean()) {
result.lenient(randomBoolean());
}
if (randomBoolean()) {
result.lowercaseExpandedTerms(randomBoolean());
}
if (randomBoolean()) {
result.locale(randomLocale(getRandom()));
}
if (randomBoolean()) {
result.minimumShouldMatch(randomFrom(MINIMUM_SHOULD_MATCH));
}
if (randomBoolean()) {
result.analyzer("simple");
}
if (randomBoolean()) {
result.defaultOperator(randomFrom(Operator.AND, Operator.OR));
}
if (randomBoolean()) {
result.boost(2.0f / randomIntBetween(1, 20));
}
if (randomBoolean()) {
Set<SimpleQueryStringFlag> flagSet = new HashSet<>();
int size = randomIntBetween(0, SimpleQueryStringFlag.values().length);
for (int i = 0; i < size; i++) {
randomFrom(SimpleQueryStringFlag.values());
}
if (flagSet.size() > 0) {
result.flags(flagSet.toArray(new SimpleQueryStringFlag[flagSet.size()]));
}
}
int fieldCount = randomIntBetween(0, 10);
Map<String, Float> fields = new TreeMap<>();
for (int i = 0; i < fieldCount; i++) {
if (randomBoolean()) {
fields.put(randomAsciiOfLengthBetween(1, 10), 1.0f);
} else {
fields.put(randomAsciiOfLengthBetween(1, 10), 2.0f / randomIntBetween(1, 20));
}
}
result.fields(fields);
return result;
}
@Test
public void testDefaults() {
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox.");
assertEquals("Wrong default default boost.", 1.0f, qb.boost(), 0.001);
assertEquals("Wrong default default boost field.", 1.0f, SimpleQueryStringBuilder.DEFAULT_BOOST, 0.001);
assertEquals("Wrong default flags.", SimpleQueryStringFlag.ALL.value, qb.flags());
assertEquals("Wrong default flags field.", SimpleQueryStringFlag.ALL.value(), SimpleQueryStringBuilder.DEFAULT_FLAGS);
assertEquals("Wrong default default operator.", Operator.OR, qb.defaultOperator());
assertEquals("Wrong default default operator field.", Operator.OR, SimpleQueryStringBuilder.DEFAULT_OPERATOR);
assertEquals("Wrong default default locale.", Locale.ROOT, qb.locale());
assertEquals("Wrong default default locale field.", Locale.ROOT, SimpleQueryStringBuilder.DEFAULT_LOCALE);
assertEquals("Wrong default default analyze_wildcard.", false, qb.analyzeWildcard());
assertEquals("Wrong default default analyze_wildcard field.", false, SimpleQueryStringBuilder.DEFAULT_ANALYZE_WILDCARD);
assertEquals("Wrong default default lowercase_expanded_terms.", true, qb.lowercaseExpandedTerms());
assertEquals("Wrong default default lowercase_expanded_terms field.", true, SimpleQueryStringBuilder.DEFAULT_LOWERCASE_EXPANDED_TERMS);
assertEquals("Wrong default default lenient.", false, qb.lenient());
assertEquals("Wrong default default lenient field.", false, SimpleQueryStringBuilder.DEFAULT_LENIENT);
assertEquals("Wrong default default locale.", Locale.ROOT, qb.locale());
assertEquals("Wrong default default locale field.", Locale.ROOT, SimpleQueryStringBuilder.DEFAULT_LOCALE);
}
@Test
public void testDefaultNullLocale() {
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox.");
qb.locale(null);
assertEquals("Setting locale to null should result in returning to default value.",
SimpleQueryStringBuilder.DEFAULT_LOCALE, qb.locale());
}
@Test
public void testDefaultNullComplainFlags() {
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox.");
qb.flags((SimpleQueryStringFlag[]) null);
assertEquals("Setting flags to null should result in returning to default value.",
SimpleQueryStringBuilder.DEFAULT_FLAGS, qb.flags());
}
@Test
public void testDefaultEmptyComplainFlags() {
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox.");
qb.flags(new SimpleQueryStringFlag[]{});
assertEquals("Setting flags to empty should result in returning to default value.",
SimpleQueryStringBuilder.DEFAULT_FLAGS, qb.flags());
}
@Test
public void testDefaultNullComplainOp() {
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox.");
qb.defaultOperator(null);
assertEquals("Setting operator to null should result in returning to default value.",
SimpleQueryStringBuilder.DEFAULT_OPERATOR, qb.defaultOperator());
}
// Check operator handling, and default field handling.
@Test
public void testDefaultOperatorHandling() {
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder("The quick brown fox.");
BooleanQuery boolQuery = (BooleanQuery) qb.toQuery(createContext());
assertThat(shouldClauses(boolQuery), is(4));
qb.defaultOperator(Operator.AND);
boolQuery = (BooleanQuery) qb.toQuery(createContext());
assertThat(shouldClauses(boolQuery), is(0));
qb.defaultOperator(Operator.OR);
boolQuery = (BooleanQuery) qb.toQuery(createContext());
assertThat(shouldClauses(boolQuery), is(4));
}
@Test
public void testValidation() {
SimpleQueryStringBuilder qb = createTestQueryBuilder();
assertNull(qb.validate());
}
@Test
public void testNullQueryTextGeneratesException() {
SimpleQueryStringBuilder builder = new SimpleQueryStringBuilder(null);
QueryValidationException exception = builder.validate();
assertThat(exception, notNullValue());
}
@Test
public void testHandlingDefaults() throws IOException {
SimpleQueryStringBuilder qb = createTestQueryBuilder();
qb.analyzer(null);
qb.minimumShouldMatch(null);
qb.queryName(null);
assertEquals(qb.toQuery(createContext()), createExpectedQuery(qb, createContext()));
}
@Test(expected = IllegalArgumentException.class)
public void testFieldCannotBeNull() {
SimpleQueryStringBuilder qb = createTestQueryBuilder();
qb.field(null);
}
@Test(expected = IllegalArgumentException.class)
public void testFieldCannotBeNullAndWeighted() {
SimpleQueryStringBuilder qb = createTestQueryBuilder();
qb.field(null, 1.0f);
}
@Test(expected = IllegalArgumentException.class)
public void testFieldCannotBeEmpty() {
SimpleQueryStringBuilder qb = createTestQueryBuilder();
qb.field("");
}
@Test(expected = IllegalArgumentException.class)
public void testFieldCannotBeEmptyAndWeighted() {
SimpleQueryStringBuilder qb = createTestQueryBuilder();
qb.field("", 1.0f);
}
/**
* The following should fail fast - never silently set the map containing
* fields and weights to null but refuse to accept null instead.
* */
@Test(expected = NullPointerException.class)
public void testFieldsCannotBeSetToNull() {
SimpleQueryStringBuilder qb = createTestQueryBuilder();
qb.fields(null);
}
@Override
protected void assertLuceneQuery(SimpleQueryStringBuilder queryBuilder, Query query, QueryParseContext context) {
if (queryBuilder.queryName() != null) {
Query namedQuery = context.copyNamedFilters().get(queryBuilder.queryName());
assertThat(namedQuery, equalTo(query));
}
}
private int shouldClauses(BooleanQuery query) {
int result = 0;
for (BooleanClause c : query.clauses()) {
if (c.getOccur() == BooleanClause.Occur.SHOULD) {
result++;
}
}
return result;
}
@Override
protected Query createExpectedQuery(SimpleQueryStringBuilder queryBuilder, QueryParseContext context) throws IOException {
Map<String, Float> fields = new TreeMap<>();
// Use the default field (_all) if no fields specified
if (queryBuilder.fields().isEmpty()) {
String field = context.defaultField();
fields.put(field, 1.0F);
} else {
fields.putAll(queryBuilder.fields());
}
// Use standard analyzer by default if none specified
Analyzer luceneAnalyzer;
if (queryBuilder.analyzer() == null) {
luceneAnalyzer = context.mapperService().searchAnalyzer();
} else {
luceneAnalyzer = context.analysisService().analyzer(queryBuilder.analyzer());
}
SimpleQueryParser sqp = new SimpleQueryParser(luceneAnalyzer, fields, queryBuilder.flags(), new Settings(queryBuilder.locale(),
queryBuilder.lowercaseExpandedTerms(), queryBuilder.lenient(), queryBuilder.analyzeWildcard()));
if (queryBuilder.defaultOperator() != null) {
switch (queryBuilder.defaultOperator()) {
case OR:
sqp.setDefaultOperator(Occur.SHOULD);
break;
case AND:
sqp.setDefaultOperator(Occur.MUST);
break;
}
}
Query query = sqp.parse(queryBuilder.text());
if (queryBuilder.queryName() != null) {
context.addNamedQuery(queryBuilder.queryName(), query);
}
if (queryBuilder.minimumShouldMatch() != null && query instanceof BooleanQuery) {
Queries.applyMinimumShouldMatch((BooleanQuery) query, queryBuilder.minimumShouldMatch());
}
query.setBoost(queryBuilder.boost());
return query;
}
}