org.codelibs.elasticsearch.index.query.SimpleQueryStringBuilder Maven / Gradle / Ivy
/*
* 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.codelibs.elasticsearch.index.query;
import org.apache.lucene.search.Query;
import org.codelibs.elasticsearch.Version;
import org.codelibs.elasticsearch.common.ParseField;
import org.codelibs.elasticsearch.common.ParsingException;
import org.codelibs.elasticsearch.common.Strings;
import org.codelibs.elasticsearch.common.io.stream.StreamInput;
import org.codelibs.elasticsearch.common.io.stream.StreamOutput;
import org.codelibs.elasticsearch.common.xcontent.XContentBuilder;
import org.codelibs.elasticsearch.common.xcontent.XContentParser;
import org.codelibs.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.Optional;
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. It supports
* the following:
*
* - '{@code +}' specifies {@code AND} operation: token1+token2
*
- '{@code |}' specifies {@code OR} operation: token1|token2
*
- '{@code -}' negates a single token: -token0
*
- '{@code "}' creates phrases of terms: "term1 term2 ..."
*
- '{@code *}' at the end of terms specifies prefix query: term*
*
- '{@code (}' and '{@code)}' specifies precedence: token1 + (token2 | token3)
*
- '{@code ~}N' at the end of terms specifies fuzzy query: term~1
*
- '{@code ~}N' at the end of phrases specifies near/slop query: "term1 term2"~5
*
*
* See: {SimpleQueryParser} for more information.
*
* This query supports these options:
*
* Required:
* {@code query} - query text to be converted into other queries
*
* Optional:
* {@code analyzer} - anaylzer to be used for analyzing tokens to determine
* which kind of query they should be converted into, defaults to "standard"
* {@code default_operator} - default operator for boolean queries, defaults
* to OR
* {@code fields} - fields to search, defaults to _all if not set, allows
* boosting a field with ^n
*
* For more detailed explanation of the query string syntax see also the online documentation.
*/
public class SimpleQueryStringBuilder extends AbstractQueryBuilder {
/** 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 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 static final ParseField MINIMUM_SHOULD_MATCH_FIELD = new ParseField("minimum_should_match");
private static final ParseField ANALYZE_WILDCARD_FIELD = new ParseField("analyze_wildcard");
private static final ParseField LENIENT_FIELD = new ParseField("lenient");
private static final ParseField LOWERCASE_EXPANDED_TERMS_FIELD = new ParseField("lowercase_expanded_terms")
.withAllDeprecated("Decision is now made by the analyzer");
private static final ParseField LOCALE_FIELD = new ParseField("locale")
.withAllDeprecated("Decision is now made by the analyzer");
private static final ParseField FLAGS_FIELD = new ParseField("flags");
private static final ParseField DEFAULT_OPERATOR_FIELD = new ParseField("default_operator");
private static final ParseField ANALYZER_FIELD = new ParseField("analyzer");
private static final ParseField QUERY_FIELD = new ParseField("query");
private static final ParseField FIELDS_FIELD = new ParseField("fields");
private static final ParseField QUOTE_FIELD_SUFFIX_FIELD = new ParseField("quote_field_suffix");
private static final ParseField ALL_FIELDS_FIELD = new ParseField("all_fields");
/** Query text to parse. */
private final String queryText;
/**
* 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 fieldsAndWeights = new TreeMap<>();
/** If specified, analyzer to use to parse the query text, defaults to registered default in toQuery. */
private String analyzer;
/** 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;
/** Any search flags to be used, ALL by default. */
private int flags = DEFAULT_FLAGS;
/** Flag specifying whether query should be forced to expand to all searchable fields */
private Boolean useAllFields;
/** Further search settings needed by the ES specific query string parser only. */
private Settings settings = new Settings();
/** Construct a new simple query with this query string. */
public SimpleQueryStringBuilder(String queryText) {
if (queryText == null) {
throw new IllegalArgumentException("query text missing");
}
this.queryText = queryText;
}
/**
* Read from a stream.
*/
public SimpleQueryStringBuilder(StreamInput in) throws IOException {
super(in);
queryText = in.readString();
int size = in.readInt();
Map fields = new HashMap<>();
for (int i = 0; i < size; i++) {
String field = in.readString();
Float weight = in.readFloat();
fields.put(field, weight);
}
fieldsAndWeights.putAll(fields);
flags = in.readInt();
analyzer = in.readOptionalString();
defaultOperator = Operator.readFromStream(in);
if (in.getVersion().before(Version.V_5_1_1_UNRELEASED)) {
in.readBoolean(); // lowercase_expanded_terms
}
settings.lenient(in.readBoolean());
settings.analyzeWildcard(in.readBoolean());
if (in.getVersion().before(Version.V_5_1_1_UNRELEASED)) {
in.readString(); // locale
}
minimumShouldMatch = in.readOptionalString();
if (in.getVersion().onOrAfter(Version.V_5_1_1_UNRELEASED)) {
settings.quoteFieldSuffix(in.readOptionalString());
useAllFields = in.readOptionalBoolean();
}
}
@Override
protected void doWriteTo(StreamOutput out) throws IOException {
out.writeString(queryText);
out.writeInt(fieldsAndWeights.size());
for (Map.Entry entry : fieldsAndWeights.entrySet()) {
out.writeString(entry.getKey());
out.writeFloat(entry.getValue());
}
out.writeInt(flags);
out.writeOptionalString(analyzer);
defaultOperator.writeTo(out);
if (out.getVersion().before(Version.V_5_1_1_UNRELEASED)) {
out.writeBoolean(true); // lowercase_expanded_terms
}
out.writeBoolean(settings.lenient());
out.writeBoolean(settings.analyzeWildcard());
if (out.getVersion().before(Version.V_5_1_1_UNRELEASED)) {
out.writeString(Locale.ROOT.toLanguageTag()); // locale
}
out.writeOptionalString(minimumShouldMatch);
if (out.getVersion().onOrAfter(Version.V_5_1_1_UNRELEASED)) {
out.writeOptionalString(settings.quoteFieldSuffix());
out.writeOptionalBoolean(useAllFields);
}
}
/** Returns the text to parse the query from. */
public String value() {
return this.queryText;
}
/** Add a field to run the query against. */
public SimpleQueryStringBuilder field(String field) {
if (Strings.isEmpty(field)) {
throw new IllegalArgumentException("supplied field is null or empty");
}
this.fieldsAndWeights.put(field, AbstractQueryBuilder.DEFAULT_BOOST);
return this;
}
/** Add a field to run the query against with a specific boost. */
public SimpleQueryStringBuilder field(String field, float boost) {
if (Strings.isEmpty(field)) {
throw new IllegalArgumentException("supplied field is null or empty");
}
this.fieldsAndWeights.put(field, boost);
return this;
}
/** Add several fields to run the query against with a specific boost. */
public SimpleQueryStringBuilder fields(Map fields) {
Objects.requireNonNull(fields, "fields cannot be null");
this.fieldsAndWeights.putAll(fields);
return this;
}
/** Returns the fields including their respective boosts to run the query against. */
public Map fields() {
return this.fieldsAndWeights;
}
/** Specify an analyzer to use for the query. */
public SimpleQueryStringBuilder analyzer(String analyzer) {
this.analyzer = analyzer;
return this;
}
/** Returns the analyzer to use for the query. */
public String analyzer() {
return this.analyzer;
}
public Boolean useAllFields() {
return useAllFields;
}
public SimpleQueryStringBuilder useAllFields(Boolean useAllFields) {
this.useAllFields = useAllFields;
return this;
}
/**
* 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) {
if (flags != null && flags.length > 0) {
int value = 0;
for (SimpleQueryStringFlag flag : flags) {
value |= flag.value;
}
this.flags = value;
} else {
this.flags = DEFAULT_FLAGS;
}
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 suffix to append to field names for phrase matching.
*/
public SimpleQueryStringBuilder quoteFieldSuffix(String suffix) {
settings.quoteFieldSuffix(suffix);
return this;
}
/**
* Return the suffix to append to field names for phrase matching.
*/
public String quoteFieldSuffix() {
return settings.quoteFieldSuffix();
}
/** Specifies whether query parsing should be lenient. Defaults to false. */
public SimpleQueryStringBuilder lenient(boolean 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.settings.analyzeWildcard(analyzeWildcard);
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;
}
@Override
protected Query doToQuery(QueryShardContext context) throws IOException {
throw new UnsupportedOperationException("querybuilders does not support this operation.");
}
@Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME);
builder.field(QUERY_FIELD.getPreferredName(), queryText);
if (fieldsAndWeights.size() > 0) {
builder.startArray(FIELDS_FIELD.getPreferredName());
for (Map.Entry entry : fieldsAndWeights.entrySet()) {
builder.value(entry.getKey() + "^" + entry.getValue());
}
builder.endArray();
}
if (analyzer != null) {
builder.field(ANALYZER_FIELD.getPreferredName(), analyzer);
}
builder.field(FLAGS_FIELD.getPreferredName(), flags);
builder.field(DEFAULT_OPERATOR_FIELD.getPreferredName(), defaultOperator.name().toLowerCase(Locale.ROOT));
builder.field(LENIENT_FIELD.getPreferredName(), settings.lenient());
builder.field(ANALYZE_WILDCARD_FIELD.getPreferredName(), settings.analyzeWildcard());
if (settings.quoteFieldSuffix() != null) {
builder.field(QUOTE_FIELD_SUFFIX_FIELD.getPreferredName(), settings.quoteFieldSuffix());
}
if (minimumShouldMatch != null) {
builder.field(MINIMUM_SHOULD_MATCH_FIELD.getPreferredName(), minimumShouldMatch);
}
if (useAllFields != null) {
builder.field(ALL_FIELDS_FIELD.getPreferredName(), useAllFields);
}
printBoostAndQueryName(builder);
builder.endObject();
}
public static Optional fromXContent(QueryParseContext parseContext) throws IOException {
XContentParser parser = parseContext.parser();
String currentFieldName = null;
String queryBody = null;
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
String queryName = null;
String minimumShouldMatch = null;
Map fieldsAndWeights = new HashMap<>();
Operator defaultOperator = null;
String analyzerName = null;
int flags = SimpleQueryStringFlag.ALL.value();
boolean lenient = SimpleQueryStringBuilder.DEFAULT_LENIENT;
boolean analyzeWildcard = SimpleQueryStringBuilder.DEFAULT_ANALYZE_WILDCARD;
String quoteFieldSuffix = null;
Boolean useAllFields = 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) {
if (FIELDS_FIELD.match(currentFieldName)) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
String fField = null;
float fBoost = 1;
char[] text = parser.textCharacters();
int end = parser.textOffset() + parser.textLength();
for (int i = parser.textOffset(); i < end; i++) {
if (text[i] == '^') {
int relativeLocation = i - parser.textOffset();
fField = new String(text, parser.textOffset(), relativeLocation);
fBoost = Float.parseFloat(new String(text, i + 1, parser.textLength() - relativeLocation - 1));
break;
}
}
if (fField == null) {
fField = parser.text();
}
fieldsAndWeights.put(fField, fBoost);
}
} else {
throw new ParsingException(parser.getTokenLocation(), "[" + SimpleQueryStringBuilder.NAME +
"] query does not support [" + currentFieldName + "]");
}
} else if (token.isValue()) {
if (QUERY_FIELD.match(currentFieldName)) {
queryBody = parser.text();
} else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName)) {
boost = parser.floatValue();
} else if (ANALYZER_FIELD.match(currentFieldName)) {
analyzerName = parser.text();
} else if (DEFAULT_OPERATOR_FIELD.match(currentFieldName)) {
defaultOperator = Operator.fromString(parser.text());
} else if (FLAGS_FIELD.match(currentFieldName)) {
if (parser.currentToken() != XContentParser.Token.VALUE_NUMBER) {
// Possible options are:
// ALL, NONE, AND, OR, PREFIX, PHRASE, PRECEDENCE, ESCAPE, WHITESPACE, FUZZY, NEAR, SLOP
flags = SimpleQueryStringFlag.resolveFlags(parser.text());
} else {
flags = parser.intValue();
if (flags < 0) {
flags = SimpleQueryStringFlag.ALL.value();
}
}
} else if (LOCALE_FIELD.match(currentFieldName)) {
// ignore, deprecated setting
} else if (LOWERCASE_EXPANDED_TERMS_FIELD.match(currentFieldName)) {
// ignore, deprecated setting
} else if (LENIENT_FIELD.match(currentFieldName)) {
lenient = parser.booleanValue();
} else if (ANALYZE_WILDCARD_FIELD.match(currentFieldName)) {
analyzeWildcard = parser.booleanValue();
} else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName)) {
queryName = parser.text();
} else if (MINIMUM_SHOULD_MATCH_FIELD.match(currentFieldName)) {
minimumShouldMatch = parser.textOrNull();
} else if (QUOTE_FIELD_SUFFIX_FIELD.match(currentFieldName)) {
quoteFieldSuffix = parser.textOrNull();
} else if (ALL_FIELDS_FIELD.match(currentFieldName)) {
useAllFields = parser.booleanValue();
} else {
throw new ParsingException(parser.getTokenLocation(), "[" + SimpleQueryStringBuilder.NAME +
"] unsupported field [" + parser.currentName() + "]");
}
} else {
throw new ParsingException(parser.getTokenLocation(), "[" + SimpleQueryStringBuilder.NAME +
"] unknown token [" + token + "] after [" + currentFieldName + "]");
}
}
// Query text is required
if (queryBody == null) {
throw new ParsingException(parser.getTokenLocation(), "[" + SimpleQueryStringBuilder.NAME + "] query text missing");
}
if ((useAllFields != null && useAllFields) && (fieldsAndWeights.size() != 0)) {
throw new ParsingException(parser.getTokenLocation(),
"cannot use [all_fields] parameter in conjunction with [fields]");
}
SimpleQueryStringBuilder qb = new SimpleQueryStringBuilder(queryBody);
qb.boost(boost).fields(fieldsAndWeights).analyzer(analyzerName).queryName(queryName).minimumShouldMatch(minimumShouldMatch);
qb.flags(flags).defaultOperator(defaultOperator);
qb.lenient(lenient).analyzeWildcard(analyzeWildcard).boost(boost).quoteFieldSuffix(quoteFieldSuffix);
qb.useAllFields(useAllFields);
return Optional.of(qb);
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
protected int doHashCode() {
return Objects.hash(fieldsAndWeights, analyzer, defaultOperator, queryText, minimumShouldMatch, settings, flags, useAllFields);
}
@Override
protected boolean doEquals(SimpleQueryStringBuilder other) {
return Objects.equals(fieldsAndWeights, other.fieldsAndWeights) && Objects.equals(analyzer, other.analyzer)
&& Objects.equals(defaultOperator, other.defaultOperator) && Objects.equals(queryText, other.queryText)
&& Objects.equals(minimumShouldMatch, other.minimumShouldMatch)
&& Objects.equals(settings, other.settings)
&& (flags == other.flags)
&& (useAllFields == other.useAllFields);
}
}