All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.index.query.SimpleQueryStringBuilder Maven / Gradle / Ivy

There is a newer version: 8.14.1
Show newest version
/*
 * 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.BooleanQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
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.regex.Regex;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.mapper.MappedFieldType;
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.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: {@link 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)) { in.readBoolean(); // lowercase_expanded_terms } settings.lenient(in.readBoolean()); settings.analyzeWildcard(in.readBoolean()); if (in.getVersion().before(Version.V_5_1_1)) { in.readString(); // locale } minimumShouldMatch = in.readOptionalString(); if (in.getVersion().onOrAfter(Version.V_5_1_1)) { 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)) { out.writeBoolean(true); // lowercase_expanded_terms } out.writeBoolean(settings.lenient()); out.writeBoolean(settings.analyzeWildcard()); if (out.getVersion().before(Version.V_5_1_1)) { out.writeString(Locale.ROOT.toLanguageTag()); // locale } out.writeOptionalString(minimumShouldMatch); if (out.getVersion().onOrAfter(Version.V_5_1_1)) { 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 { // field names in builder can have wildcards etc, need to resolve them here Map resolvedFieldsAndWeights = new TreeMap<>(); if ((useAllFields != null && useAllFields) && (fieldsAndWeights.size() != 0)) { throw addValidationError("cannot use [all_fields] parameter in conjunction with [fields]", null); } // If explicitly required to use all fields, use all fields, OR: // Automatically determine the fields (to replace the _all field) if all of the following are true: // - The _all field is disabled, // - and the default_field has not been changed in the settings // - and no fields are specified in the request Settings newSettings = new Settings(settings); if ((this.useAllFields != null && this.useAllFields) || (context.getMapperService().allEnabled() == false && "_all".equals(context.defaultField()) && this.fieldsAndWeights.isEmpty())) { resolvedFieldsAndWeights = QueryStringQueryBuilder.allQueryableDefaultFields(context); // Need to use lenient mode when using "all-mode" so exceptions aren't thrown due to mismatched types newSettings.lenient(true); } else { // Use the default field if no fields specified if (fieldsAndWeights.isEmpty()) { resolvedFieldsAndWeights.put(resolveIndexName(context.defaultField(), context), AbstractQueryBuilder.DEFAULT_BOOST); } else { for (Map.Entry fieldEntry : fieldsAndWeights.entrySet()) { if (Regex.isSimpleMatchPattern(fieldEntry.getKey())) { for (String fieldName : context.getMapperService().simpleMatchToIndexNames(fieldEntry.getKey())) { resolvedFieldsAndWeights.put(fieldName, fieldEntry.getValue()); } } else { resolvedFieldsAndWeights.put(resolveIndexName(fieldEntry.getKey(), context), fieldEntry.getValue()); } } } } // Use standard analyzer by default if none specified Analyzer luceneAnalyzer; if (analyzer == null) { luceneAnalyzer = context.getMapperService().searchAnalyzer(); } else { luceneAnalyzer = context.getIndexAnalyzers().get(analyzer); if (luceneAnalyzer == null) { throw new QueryShardException(context, "[" + SimpleQueryStringBuilder.NAME + "] analyzer [" + analyzer + "] not found"); } } SimpleQueryParser sqp = new SimpleQueryParser(luceneAnalyzer, resolvedFieldsAndWeights, flags, newSettings, context); sqp.setDefaultOperator(defaultOperator.toBooleanClauseOccur()); Query query = sqp.parse(queryText); // If the coordination factor is disabled on a boolean query we don't apply the minimum should match. // This is done to make sure that the minimum_should_match doesn't get applied when there is only one word // and multiple variations of the same word in the query (synonyms for instance). if (minimumShouldMatch != null && query instanceof BooleanQuery && !((BooleanQuery) query).isCoordDisabled()) { query = Queries.applyMinimumShouldMatch((BooleanQuery) query, minimumShouldMatch); } return query; } private static String resolveIndexName(String fieldName, QueryShardContext context) { MappedFieldType fieldType = context.fieldMapper(fieldName); if (fieldType != null) { return fieldType.name(); } return fieldName; } @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 (parseContext.getParseFieldMatcher().match(currentFieldName, FIELDS_FIELD)) { 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 (parseContext.getParseFieldMatcher().match(currentFieldName, QUERY_FIELD)) { queryBody = parser.text(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, AbstractQueryBuilder.BOOST_FIELD)) { boost = parser.floatValue(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, ANALYZER_FIELD)) { analyzerName = parser.text(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, DEFAULT_OPERATOR_FIELD)) { defaultOperator = Operator.fromString(parser.text()); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, FLAGS_FIELD)) { 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 (parseContext.getParseFieldMatcher().match(currentFieldName, LOCALE_FIELD)) { // ignore, deprecated setting } else if (parseContext.getParseFieldMatcher().match(currentFieldName, LOWERCASE_EXPANDED_TERMS_FIELD)) { // ignore, deprecated setting } else if (parseContext.getParseFieldMatcher().match(currentFieldName, LENIENT_FIELD)) { lenient = parser.booleanValue(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, ANALYZE_WILDCARD_FIELD)) { analyzeWildcard = parser.booleanValue(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, AbstractQueryBuilder.NAME_FIELD)) { queryName = parser.text(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, MINIMUM_SHOULD_MATCH_FIELD)) { minimumShouldMatch = parser.textOrNull(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, QUOTE_FIELD_SUFFIX_FIELD)) { quoteFieldSuffix = parser.textOrNull(); } else if (parseContext.getParseFieldMatcher().match(currentFieldName, ALL_FIELDS_FIELD)) { 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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy