/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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.apache.solr.search; import org.apache.lucene.index.Term; import org.apache.lucene.legacy.LegacyNumericRangeQuery; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.CharsRefBuilder; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.parser.QueryParser; import org.apache.solr.schema.FieldType; import org.apache.solr.schema.IndexSchema; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Collection of static utilities useful for query parsing. * * */ public class QueryParsing { public static final String OP = "q.op"; // the SolrParam used to override the QueryParser "default operator" public static final String V = "v"; // value of this parameter public static final String F = "f"; // field that a query or command pertains to public static final String TYPE = "type";// parser for this query or command public static final String DEFTYPE = "defType"; // default parser for any direct subqueries public static final String SPLIT_ON_WHITESPACE = "sow"; // Whether to split on whitespace prior to analysis public static final String LOCALPARAM_START = "{!"; public static final char LOCALPARAM_END = '}'; // true if the value was specified by the "v" param (i.e. v=myval, or v=$param) public static final String VAL_EXPLICIT = "__VAL_EXPLICIT__"; /** * Returns the default operator for use by Query Parsers, parsed from the df string * @param notUsed is not used, but is there for back compat with 3rd party QParsers * @param df the df string from request * @deprecated this method is here purely not to break code back compat in 7.x */ @Deprecated public static QueryParser.Operator getQueryParserDefaultOperator(final IndexSchema notUsed, final String df) { return parseOP(df); } /** * Returns the effective default field based on the 'df' param. * TODO: This is kept for 3rd party QParser compat in 7.x. Remove this method in Solr 8.0 * @param ignored Not in use * @param df the default field, which will be returned as-is * @see org.apache.solr.common.params.CommonParams#DF * @deprecated IndexScema does not contain defaultField anymore, you must rely on df alone */ @Deprecated public static String getDefaultField(final IndexSchema ignored, final String df) { return df; } /** * @param txt Text to parse * @param start Index into text for start of parsing * @param target Object to inject with parsed settings * @param params Additional existing parameters */ public static int parseLocalParams(String txt, int start, ModifiableSolrParams target, SolrParams params) throws SyntaxError { return parseLocalParams(txt, start, target, params, LOCALPARAM_START, LOCALPARAM_END); } /** * @param txt Text to parse * @param start Index into text for start of parsing * @param target Object to inject with parsed settings * @param params Additional existing parameters * @param startString String that indicates the start of a localParams section * @param endChar Character that indicates the end of a localParams section */ public static int parseLocalParams(String txt, int start, ModifiableSolrParams target, SolrParams params, String startString, char endChar) throws SyntaxError { int off = start; if (!txt.startsWith(startString, off)) return start; StrParser p = new StrParser(txt, start, txt.length()); p.pos += startString.length(); // skip over "{!" for (; ;) { /* if (p.pos>=txt.length()) { throw new SyntaxError("Missing '}' parsing local params '" + txt + '"'); } */ char ch = p.peek(); if (ch == endChar) { return p.pos + 1; } String id = p.getId(); if (id.length() == 0) { throw new SyntaxError("Expected ending character '" + endChar + "' parsing local params '" + txt + '"'); } String val = null; ch = p.peek(); if (ch != '=') { // single word... treat {!func} as type=func for easy lookup val = id; id = TYPE; } else { // saw equals, so read value p.pos++; ch = p.peek(); boolean deref = false; if (ch == '$') { p.pos++; ch = p.peek(); deref = true; // dereference whatever value is read by treating it as a variable name } if (ch == '\"' || ch == '\'') { val = p.getQuotedString(); } else { // read unquoted literal ended by whitespace or endChar (normally '}') // there is no escaping. int valStart = p.pos; for (; ;) { if (p.pos >= p.end) { throw new SyntaxError("Missing end to unquoted value starting at " + valStart + " str='" + txt + "'"); } char c = p.val.charAt(p.pos); if (c == endChar || Character.isWhitespace(c)) { val = p.val.substring(valStart, p.pos); break; } p.pos++; } } if (deref) { // dereference parameter if (params != null) { val = params.get(val); } } } if (target != null) target.add(id, val); } } /** * "foo" returns null * "{!prefix f=myfield}yes" returns type="prefix",f="myfield",v="yes" * "{!prefix f=myfield v=$p}" returns type="prefix",f="myfield",v=params.get("p") */ public static SolrParams getLocalParams(String txt, SolrParams params) throws SyntaxError { if (txt == null || !txt.startsWith(LOCALPARAM_START)) { return null; } ModifiableSolrParams localParams = new ModifiableSolrParams(); int start = QueryParsing.parseLocalParams(txt, 0, localParams, params); String val = localParams.get(V); if (val == null) { val = txt.substring(start); localParams.set(V, val); } else { // localParams.put(VAL_EXPLICIT, "true"); } return localParams; } /////////////////////////// /////////////////////////// /////////////////////////// static FieldType writeFieldName(String name, IndexSchema schema, Appendable out, int flags) throws IOException { FieldType ft = null; ft = schema.getFieldTypeNoEx(name); out.append(name); if (ft == null) { out.append("(UNKNOWN FIELD " + name + ')'); } out.append(':'); return ft; } static void writeFieldVal(String val, FieldType ft, Appendable out, int flags) throws IOException { if (ft != null) { try { out.append(ft.indexedToReadable(val)); } catch (Exception e) { out.append("EXCEPTION(val="); out.append(val); out.append(")"); } } else { out.append(val); } } static void writeFieldVal(BytesRef val, FieldType ft, Appendable out, int flags) throws IOException { if (ft != null) { try { CharsRefBuilder readable = new CharsRefBuilder(); ft.indexedToReadable(val, readable); out.append(readable.get()); } catch (Exception e) { out.append("EXCEPTION(val="); out.append(val.utf8ToString()); out.append(")"); } } else { out.append(val.utf8ToString()); } } private static int FLAG_BOOSTED=0x01; private static int FLAG_IS_CLAUSE=0x02; /** * @see #toString(Query,IndexSchema) */ public static void toString(Query query, IndexSchema schema, Appendable out, int flags) throws IOException { int subflag = flags & ~(FLAG_BOOSTED|FLAG_IS_CLAUSE); // clear the boosted / is clause flags for recursion if (query instanceof TermQuery) { TermQuery q = (TermQuery) query; Term t = q.getTerm(); FieldType ft = writeFieldName(t.field(), schema, out, flags); writeFieldVal(t.bytes(), ft, out, flags); } else if (query instanceof TermRangeQuery) { TermRangeQuery q = (TermRangeQuery) query; String fname = q.getField(); FieldType ft = writeFieldName(fname, schema, out, flags); out.append(q.includesLower() ? '[' : '{'); BytesRef lt = q.getLowerTerm(); BytesRef ut = q.getUpperTerm(); if (lt == null) { out.append('*'); } else { writeFieldVal(lt, ft, out, flags); } out.append(" TO "); if (ut == null) { out.append('*'); } else { writeFieldVal(ut, ft, out, flags); } out.append(q.includesUpper() ? ']' : '}'); } else if (query instanceof LegacyNumericRangeQuery) { LegacyNumericRangeQuery q = (LegacyNumericRangeQuery) query; String fname = q.getField(); FieldType ft = writeFieldName(fname, schema, out, flags); out.append(q.includesMin() ? '[' : '{'); Number lt = q.getMin(); Number ut = q.getMax(); if (lt == null) { out.append('*'); } else { out.append(lt.toString()); } out.append(" TO "); if (ut == null) { out.append('*'); } else { out.append(ut.toString()); } out.append(q.includesMax() ? ']' : '}'); } else if (query instanceof BooleanQuery) { BooleanQuery q = (BooleanQuery) query; boolean needParens = false; if (q.getMinimumNumberShouldMatch() != 0 || (flags & (FLAG_IS_CLAUSE | FLAG_BOOSTED)) != 0 ) { needParens = true; } if (needParens) { out.append('('); } boolean first = true; for (BooleanClause c : q.clauses()) { if (!first) { out.append(' '); } else { first = false; } if (c.isProhibited()) { out.append('-'); } else if (c.isRequired()) { out.append('+'); } Query subQuery = c.getQuery(); toString(subQuery, schema, out, subflag | FLAG_IS_CLAUSE); } if (needParens) { out.append(')'); } if (q.getMinimumNumberShouldMatch() > 0) { out.append('~'); out.append(Integer.toString(q.getMinimumNumberShouldMatch())); } } else if (query instanceof PrefixQuery) { PrefixQuery q = (PrefixQuery) query; Term prefix = q.getPrefix(); FieldType ft = writeFieldName(prefix.field(), schema, out, flags); out.append(prefix.text()); out.append('*'); } else if (query instanceof WildcardQuery) { out.append(query.toString()); } else if (query instanceof FuzzyQuery) { out.append(query.toString()); } else if (query instanceof ConstantScoreQuery) { out.append(query.toString()); } else if (query instanceof WrappedQuery) { WrappedQuery q = (WrappedQuery)query; out.append(q.getOptions()); toString(q.getWrappedQuery(), schema, out, subflag); } else if (query instanceof BoostQuery) { BoostQuery q = (BoostQuery)query; toString(q.getQuery(), schema, out, subflag | FLAG_BOOSTED); out.append("^"); out.append(Float.toString(q.getBoost())); } else { out.append(query.getClass().getSimpleName() + '(' + query.toString() + ')'); } } /** * Formats a Query for debugging, using the IndexSchema to make * complex field types readable. * <p> * The benefit of using this method instead of calling * <code>Query.toString</code> directly is that it knows about the data * types of each field, so any field which is encoded in a particularly * complex way is still readable. The downside is that it only knows * about built in Query types, and will not be able to format custom * Query classes. * </p> */ public static String toString(Query query, IndexSchema schema) { try { StringBuilder sb = new StringBuilder(); toString(query, schema, sb, 0); return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } } /** * Builds a list of String which are stringified versions of a list of Queries */ public static List<String> toString(List<Query> queries, IndexSchema schema) { List<String> out = new ArrayList<>(queries.size()); for (Query q : queries) { out.add(QueryParsing.toString(q, schema)); } return out; } /** * Parses default operator string into Operator object * @param operator the string from request * @return Operator.AND if string equals "AND", else return Operator.OR (default) */ public static QueryParser.Operator parseOP(String operator) { return "and".equalsIgnoreCase(operator) ? QueryParser.Operator.AND : QueryParser.Operator.OR; } }