/* * 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.util; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.MethodDescriptor; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Pattern; import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexableField; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.DisjunctionMaxQuery; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.MapSolrParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; import org.apache.solr.common.util.StrUtils; import org.apache.solr.core.RequestParams; import org.apache.solr.handler.component.HighlightComponent; import org.apache.solr.handler.component.ResponseBuilder; import org.apache.solr.handler.component.ShardDoc; import org.apache.solr.handler.component.ShardRequest; import org.apache.solr.highlight.SolrHighlighter; import org.apache.solr.parser.QueryParser; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrRequestHandler; import org.apache.solr.request.json.RequestUtil; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.schema.IndexSchema; import org.apache.solr.schema.SchemaField; import org.apache.solr.search.CacheRegenerator; import org.apache.solr.search.DocIterator; import org.apache.solr.search.DocList; import org.apache.solr.search.DocSet; import org.apache.solr.search.FieldParams; import org.apache.solr.search.QParser; import org.apache.solr.search.QueryParsing; import org.apache.solr.search.ReturnFields; import org.apache.solr.search.SolrCache; import org.apache.solr.search.SolrIndexSearcher; import org.apache.solr.search.SolrQueryParser; import org.apache.solr.search.SortSpecParsing; import org.apache.solr.search.SyntaxError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableMap; import static java.util.Collections.singletonList; import static org.apache.solr.core.PluginInfo.APPENDS; import static org.apache.solr.core.PluginInfo.DEFAULTS; import static org.apache.solr.core.PluginInfo.INVARIANTS; import static org.apache.solr.core.RequestParams.USEPARAM; /** * <p>Utilities that may be of use to RequestHandlers.</p> * * <p> * Many of these functions have code that was stolen/mutated from * StandardRequestHandler. * </p> * * <p>:TODO: refactor StandardRequestHandler to use these utilities</p> * * <p>:TODO: Many "standard" functionality methods are not cognisant of * default parameter settings. */ public class SolrPluginUtils { /** * Map containing all the possible purposes codes of a request as key and * the corresponding readable purpose as value */ private static final Map<Integer, String> purposes; private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); static { Map<Integer, String> map = new TreeMap<>(); map.put(ShardRequest.PURPOSE_PRIVATE, "PRIVATE"); map.put(ShardRequest.PURPOSE_GET_TOP_IDS, "GET_TOP_IDS"); map.put(ShardRequest.PURPOSE_REFINE_TOP_IDS, "REFINE_TOP_IDS"); map.put(ShardRequest.PURPOSE_GET_FACETS, "GET_FACETS"); map.put(ShardRequest.PURPOSE_REFINE_FACETS, "REFINE_FACETS"); map.put(ShardRequest.PURPOSE_GET_FIELDS, "GET_FIELDS"); map.put(ShardRequest.PURPOSE_GET_HIGHLIGHTS, "GET_HIGHLIGHTS"); map.put(ShardRequest.PURPOSE_GET_DEBUG, "GET_DEBUG"); map.put(ShardRequest.PURPOSE_GET_STATS, "GET_STATS"); map.put(ShardRequest.PURPOSE_GET_TERMS, "GET_TERMS"); map.put(ShardRequest.PURPOSE_GET_TOP_GROUPS, "GET_TOP_GROUPS"); map.put(ShardRequest.PURPOSE_GET_MLT_RESULTS, "GET_MLT_RESULTS"); map.put(ShardRequest.PURPOSE_REFINE_PIVOT_FACETS, "REFINE_PIVOT_FACETS"); map.put(ShardRequest.PURPOSE_SET_TERM_STATS, "SET_TERM_STATS"); map.put(ShardRequest.PURPOSE_GET_TERM_STATS, "GET_TERM_STATS"); purposes = Collections.unmodifiableMap(map); } private static final MapSolrParams maskUseParams = new MapSolrParams(ImmutableMap.<String, String>builder() .put(USEPARAM, "") .build()); /** * Set default-ish params on a SolrQueryRequest. * * RequestHandlers can use this method to ensure their defaults and * overrides are visible to other components such as the response writer * * @param req The request whose params we are interested i * @param defaults values to be used if no values are specified in the request params * @param appends values to be appended to those from the request (or defaults) when dealing with multi-val params, or treated as another layer of defaults for singl-val params. * @param invariants values which will be used instead of any request, or default values, regardless of context. */ public static void setDefaults(SolrQueryRequest req, SolrParams defaults, SolrParams appends, SolrParams invariants) { setDefaults(null, req, defaults, appends, invariants); } public static void setDefaults(SolrRequestHandler handler, SolrQueryRequest req, SolrParams defaults, SolrParams appends, SolrParams invariants) { String useParams = (String) req.getContext().get(USEPARAM); if(useParams != null) { RequestParams rp = req.getCore().getSolrConfig().getRequestParams(); defaults = applyParamSet(rp, defaults, useParams, DEFAULTS); appends = applyParamSet(rp, appends, useParams, APPENDS); invariants = applyParamSet(rp, invariants, useParams, INVARIANTS); } useParams = req.getParams().get(USEPARAM); if (useParams != null && !useParams.isEmpty()) { RequestParams rp = req.getCore().getSolrConfig().getRequestParams(); // now that we have expanded the request macro useParams with the actual values // it makes no sense to keep it visible now on. // distrib request sends all params to the nodes down the line and // if it sends the useParams to other nodes , they will expand them as well. // which is not desirable. At the same time, because we send the useParams // value as an empty string to other nodes we get the desired benefit of // overriding the useParams specified in the requestHandler directly req.setParams(SolrParams.wrapDefaults(maskUseParams, req.getParams())); defaults = applyParamSet(rp, defaults, useParams, DEFAULTS); appends = applyParamSet(rp, appends, useParams, APPENDS); invariants = applyParamSet(rp, invariants, useParams, INVARIANTS); } RequestUtil.processParams(handler, req, defaults, appends, invariants); } private static SolrParams applyParamSet(RequestParams requestParams, SolrParams defaults, String paramSets, String type) { if (paramSets == null) return defaults; List<String> paramSetList = paramSets.indexOf(',') == -1 ? singletonList(paramSets) : StrUtils.splitSmart(paramSets, ','); for (String name : paramSetList) { RequestParams.VersionedParams params = requestParams.getParams(name, type); if (params == null) return defaults; if (type.equals(DEFAULTS)) { defaults = SolrParams.wrapDefaults(params, defaults); } else if (type.equals(INVARIANTS)) { defaults = SolrParams.wrapAppended(params, defaults); } else { defaults = SolrParams.wrapAppended(params, defaults); } } return defaults; } /** * SolrIndexSearch.numDocs(Query,Query) freaks out if the filtering * query is null, so we use this workarround. */ public static int numDocs(SolrIndexSearcher s, Query q, Query f) throws IOException { return (null == f) ? s.getDocSet(q).size() : s.numDocs(q,f); } private final static Pattern splitList=Pattern.compile(",| "); /** Split a value that may contain a comma, space of bar separated list. */ public static String[] split(String value){ return splitList.split(value.trim(), 0); } /** * Pre-fetch documents into the index searcher's document cache. * * This is an entirely optional step which you might want to perform for * the following reasons: * * <ul> * <li>Locates the document-retrieval costs in one spot, which helps * detailed performance measurement</li> * * <li>Determines a priori what fields will be needed to be fetched by * various subtasks, like response writing and highlighting. This * minimizes the chance that many needed fields will be loaded lazily. * (it is more efficient to load all the field we require normally).</li> * </ul> * * If lazy field loading is disabled, this method does nothing. */ public static void optimizePreFetchDocs(ResponseBuilder rb, DocList docs, Query query, SolrQueryRequest req, SolrQueryResponse res) throws IOException { SolrIndexSearcher searcher = req.getSearcher(); if(!searcher.getDocFetcher().isLazyFieldLoadingEnabled()) { // nothing to do return; } ReturnFields returnFields = res.getReturnFields(); if(returnFields.getLuceneFieldNames() != null) { Set<String> fieldFilter = returnFields.getLuceneFieldNames(); if (rb.doHighlights) { // copy return fields list fieldFilter = new HashSet<>(fieldFilter); // add highlight fields SolrHighlighter highlighter = HighlightComponent.getHighlighter(req.getCore()); for (String field: highlighter.getHighlightFields(query, req, null)) fieldFilter.add(field); // fetch unique key if one exists. SchemaField keyField = searcher.getSchema().getUniqueKeyField(); if(null != keyField) fieldFilter.add(keyField.getName()); } // get documents DocIterator iter = docs.iterator(); for (int i=0; i<docs.size(); i++) { searcher.doc(iter.nextDoc(), fieldFilter); } } } public static Set<String> getDebugInterests(String[] params, ResponseBuilder rb){ Set<String> debugInterests = new HashSet<>(); if (params != null) { for (int i = 0; i < params.length; i++) { if (params[i].equalsIgnoreCase("all") || params[i].equalsIgnoreCase("true")){ rb.setDebug(true); break; //still might add others } else if (params[i].equals(CommonParams.TIMING)){ rb.setDebugTimings(true); } else if (params[i].equals(CommonParams.QUERY)){ rb.setDebugQuery(true); } else if (params[i].equals(CommonParams.RESULTS)){ rb.setDebugResults(true); } else if (params[i].equals(CommonParams.TRACK)){ rb.setDebugTrack(true); } } } return debugInterests; } /** * <p> * Returns a NamedList containing many "standard" pieces of debugging * information. * </p> * * <ul> * <li>rawquerystring - the 'q' param exactly as specified by the client * </li> * <li>querystring - the 'q' param after any preprocessing done by the plugin * </li> * <li>parsedquery - the main query executed formated by the Solr * QueryParsing utils class (which knows about field types) * </li> * <li>parsedquery_toString - the main query executed formatted by its * own toString method (in case it has internal state Solr * doesn't know about) * </li> * <li>explain - the list of score explanations for each document in * results against query. * </li> * <li>otherQuery - the query string specified in 'explainOther' query param. * </li> * <li>explainOther - the list of score explanations for each document in * results against 'otherQuery' * </li> * </ul> * * @param req the request we are dealing with * @param userQuery the users query as a string, after any basic * preprocessing has been done * @param query the query built from the userQuery * (and perhaps other clauses) that identifies the main * result set of the response. * @param results the main result set of the response * @return The debug info * @throws java.io.IOException if there was an IO error */ public static NamedList doStandardDebug( SolrQueryRequest req, String userQuery, Query query, DocList results, boolean dbgQuery, boolean dbgResults) throws IOException { NamedList dbg = new SimpleOrderedMap(); doStandardQueryDebug(req, userQuery, query, dbgQuery, dbg); doStandardResultsDebug(req, query, results, dbgResults, dbg); return dbg; } public static void doStandardQueryDebug( SolrQueryRequest req, String userQuery, Query query, boolean dbgQuery, NamedList dbg) { if (dbgQuery) { /* userQuery may have been pre-processed .. expose that */ dbg.add("rawquerystring", req.getParams().get(CommonParams.Q)); dbg.add("querystring", userQuery); /* QueryParsing.toString isn't perfect, use it to see converted * values, use regular toString to see any attributes of the * underlying Query it may have missed. */ dbg.add("parsedquery", QueryParsing.toString(query, req.getSchema())); dbg.add("parsedquery_toString", query.toString()); } } public static void doStandardResultsDebug( SolrQueryRequest req, Query query, DocList results, boolean dbgResults, NamedList dbg) throws IOException { if (dbgResults) { SolrIndexSearcher searcher = req.getSearcher(); IndexSchema schema = searcher.getSchema(); boolean explainStruct = req.getParams().getBool(CommonParams.EXPLAIN_STRUCT, false); if (results != null) { NamedList<Explanation> explain = getExplanations(query, results, searcher, schema); dbg.add("explain", explainStruct ? explanationsToNamedLists(explain) : explanationsToStrings(explain)); } String otherQueryS = req.getParams().get(CommonParams.EXPLAIN_OTHER); if (otherQueryS != null && otherQueryS.length() > 0) { DocList otherResults = doSimpleQuery(otherQueryS, req, 0, 10); dbg.add("otherQuery", otherQueryS); NamedList<Explanation> explainO = getExplanations(query, otherResults, searcher, schema); dbg.add("explainOther", explainStruct ? explanationsToNamedLists(explainO) : explanationsToStrings(explainO)); } } } public static NamedList<Object> explanationToNamedList(Explanation e) { NamedList<Object> out = new SimpleOrderedMap<>(); out.add("match", e.isMatch()); out.add("value", e.getValue()); out.add("description", e.getDescription()); Explanation[] details = e.getDetails(); // short circut out if (0 == details.length) return out; List<NamedList<Object>> kids = new ArrayList<>(details.length); for (Explanation d : details) { kids.add(explanationToNamedList(d)); } out.add("details", kids); return out; } public static NamedList<NamedList<Object>> explanationsToNamedLists (NamedList<Explanation> explanations) { NamedList<NamedList<Object>> out = new SimpleOrderedMap<>(); for (Map.Entry<String,Explanation> entry : explanations) { out.add(entry.getKey(), explanationToNamedList(entry.getValue())); } return out; } /** * Generates an NamedList of Explanations for each item in a list of docs. * * @param query The Query you want explanations in the context of * @param docs The Documents you want explained relative that query */ public static NamedList<Explanation> getExplanations (Query query, DocList docs, SolrIndexSearcher searcher, IndexSchema schema) throws IOException { NamedList<Explanation> explainList = new SimpleOrderedMap<>(); DocIterator iterator = docs.iterator(); for (int i=0; i<docs.size(); i++) { int id = iterator.nextDoc(); Document doc = searcher.doc(id); String strid = schema.printableUniqueKey(doc); explainList.add(strid, searcher.explain(query, id) ); } return explainList; } private static NamedList<String> explanationsToStrings (NamedList<Explanation> explanations) { NamedList<String> out = new SimpleOrderedMap<>(); for (Map.Entry<String,Explanation> entry : explanations) { out.add(entry.getKey(), "\n"+entry.getValue().toString()); } return out; } /** * Executes a basic query */ public static DocList doSimpleQuery(String sreq, SolrQueryRequest req, int start, int limit) throws IOException { List<String> commands = StrUtils.splitSmart(sreq,';'); String qs = commands.size() >= 1 ? commands.get(0) : ""; try { Query query = QParser.getParser(qs, req).getQuery(); // If the first non-query, non-filter command is a simple sort on an indexed field, then // we can use the Lucene sort ability. Sort sort = null; if (commands.size() >= 2) { sort = SortSpecParsing.parseSortSpec(commands.get(1), req).getSort(); } DocList results = req.getSearcher().getDocList(query,(DocSet)null, sort, start, limit); return results; } catch (SyntaxError e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing query: " + qs); } } private static final Pattern whitespacePattern = Pattern.compile("\\s+"); private static final Pattern caratPattern = Pattern.compile("\\^"); private static final Pattern tildePattern = Pattern.compile("[~]"); /** * Given a string containing fieldNames and boost info, * converts it to a Map from field name to boost info. * * <p> * Doesn't care if boost info is negative, you're on your own. * </p> * <p> * Doesn't care if boost info is missing, again: you're on your own. * </p> * * @param in a String like "fieldOne^2.3 fieldTwo fieldThree^-0.4" * @return Map of fieldOne => 2.3, fieldTwo => null, fieldThree => -0.4 */ public static Map<String,Float> parseFieldBoosts(String in) { return parseFieldBoosts(new String[]{in}); } /** * Like <code>parseFieldBoosts(String)</code>, but parses all the strings * in the provided array (which may be null). * * @param fieldLists an array of Strings eg. <code>{"fieldOne^2.3", "fieldTwo", fieldThree^-0.4}</code> * @return Map of fieldOne => 2.3, fieldTwo => null, fieldThree => -0.4 */ public static Map<String,Float> parseFieldBoosts(String[] fieldLists) { if (null == fieldLists || 0 == fieldLists.length) { return new HashMap<>(); } Map<String, Float> out = new HashMap<>(7); for (String in : fieldLists) { if (null == in) { continue; } in = in.trim(); if(in.length()==0) { continue; } String[] bb = whitespacePattern.split(in); for (String s : bb) { String[] bbb = caratPattern.split(s); out.put(bbb[0], 1 == bbb.length ? null : Float.valueOf(bbb[1])); } } return out; } /** /** * Like {@link #parseFieldBoosts}, but allows for an optional slop value prefixed by "~". * * @param fieldLists - an array of Strings eg. <code>{"fieldOne^2.3", "fieldTwo", fieldThree~5^-0.4}</code> * @param wordGrams - (0=all words, 2,3 = shingle size) * @param defaultSlop - the default slop for this param * @return - FieldParams containing the fieldname,boost,slop,and shingle size */ public static List<FieldParams> parseFieldBoostsAndSlop(String[] fieldLists,int wordGrams,int defaultSlop) { if (null == fieldLists || 0 == fieldLists.length) { return new ArrayList<>(); } List<FieldParams> out = new ArrayList<>(); for (String in : fieldLists) { if (null == in) { continue; } in = in.trim(); if(in.length()==0) { continue; } String[] fieldConfigs = whitespacePattern.split(in); for (String s : fieldConfigs) { String[] fieldAndSlopVsBoost = caratPattern.split(s); String[] fieldVsSlop = tildePattern.split(fieldAndSlopVsBoost[0]); String field = fieldVsSlop[0]; int slop = (2 == fieldVsSlop.length) ? Integer.parseInt(fieldVsSlop[1]) : defaultSlop; float boost = (1 == fieldAndSlopVsBoost.length) ? 1 : Float.parseFloat(fieldAndSlopVsBoost[1]); FieldParams fp = new FieldParams(field,wordGrams,slop,boost); out.add(fp); } } return out; } /** * Checks the number of optional clauses in the query, and compares it * with the specification string to determine the proper value to use. * <p> * If mmAutoRelax=true, we'll perform auto relaxation of mm if tokens * are removed from some but not all DisMax clauses, as can happen when * stopwords or punctuation tokens are removed in analysis. * </p> * <p> * Details about the specification format can be found * <a href="doc-files/min-should-match.html">here</a> * </p> * * <p>A few important notes...</p> * <ul> * <li> * If the calculations based on the specification determine that no * optional clauses are needed, BooleanQuerysetMinMumberShouldMatch * will never be called, but the usual rules about BooleanQueries * still apply at search time (a BooleanQuery containing no required * clauses must still match at least one optional clause) * <li> * <li> * No matter what number the calculation arrives at, * BooleanQuery.setMinShouldMatch() will never be called with a * value greater then the number of optional clauses (or less then 1) * </li> * </ul> * * <p>:TODO: should optimize the case where number is same * as clauses to just make them all "required" * </p> * * @param q The query as a BooleanQuery.Builder * @param spec The mm spec * @param mmAutoRelax whether to perform auto relaxation of mm if tokens are removed from some but not all DisMax clauses */ public static void setMinShouldMatch(BooleanQuery.Builder q, String spec, boolean mmAutoRelax) { int optionalClauses = 0; int maxDisjunctsSize = 0; int optionalDismaxClauses = 0; for (BooleanClause c : q.build().clauses()) { if (c.getOccur() == Occur.SHOULD) { if (mmAutoRelax && c.getQuery() instanceof DisjunctionMaxQuery) { int numDisjuncts = ((DisjunctionMaxQuery)c.getQuery()).getDisjuncts().size(); if (numDisjuncts>maxDisjunctsSize) { maxDisjunctsSize = numDisjuncts; optionalDismaxClauses = 1; } else if (numDisjuncts == maxDisjunctsSize) { optionalDismaxClauses++; } } else { optionalClauses++; } } } int msm = calculateMinShouldMatch(optionalClauses + optionalDismaxClauses, spec); if (0 < msm) { q.setMinimumNumberShouldMatch(msm); } } public static void setMinShouldMatch(BooleanQuery.Builder q, String spec) { setMinShouldMatch(q, spec, false); } public static BooleanQuery setMinShouldMatch(BooleanQuery q, String spec) { return setMinShouldMatch(q, spec, false); } public static BooleanQuery setMinShouldMatch(BooleanQuery q, String spec, boolean mmAutoRelax) { BooleanQuery.Builder builder = new BooleanQuery.Builder(); for (BooleanClause clause : q) { builder.add(clause); } setMinShouldMatch(builder, spec, mmAutoRelax); return builder.build(); } // private static Pattern spaceAroundLessThanPattern = Pattern.compile("\\s*<\\s*"); private static Pattern spaceAroundLessThanPattern = Pattern.compile("(\\s+<\\s*)|(\\s*<\\s+)"); private static Pattern spacePattern = Pattern.compile(" "); private static Pattern lessThanPattern = Pattern.compile("<"); /** * helper exposed for UnitTests * @see #setMinShouldMatch */ static int calculateMinShouldMatch(int optionalClauseCount, String spec) { int result = optionalClauseCount; spec = spec.trim(); if (-1 < spec.indexOf("<")) { /* we have conditional spec(s) */ spec = spaceAroundLessThanPattern.matcher(spec).replaceAll("<"); for (String s : spacePattern.split(spec)) { String[] parts = lessThanPattern.split(s,0); int upperBound = Integer.parseInt(parts[0]); if (optionalClauseCount <= upperBound) { return result; } else { result = calculateMinShouldMatch (optionalClauseCount, parts[1]); } } return result; } /* otherwise, simple expresion */ if (-1 < spec.indexOf('%')) { /* percentage - assume the % was the last char. If not, let Integer.parseInt fail. */ spec = spec.substring(0,spec.length()-1); int percent = Integer.parseInt(spec); float calc = (result * percent) * (1/100f); result = calc < 0 ? result + (int)calc : (int)calc; } else { int calc = Integer.parseInt(spec); result = calc < 0 ? result + calc : calc; } return (optionalClauseCount < result ? optionalClauseCount : (result < 0 ? 0 : result)); } /** * Recursively walks the "from" query pulling out sub-queries and * adding them to the "to" query. * * <p> * Boosts are multiplied as needed. Sub-BooleanQueryies which are not * optional will not be flattened. From will be mangled durring the walk, * so do not attempt to reuse it. * </p> */ public static void flattenBooleanQuery(BooleanQuery.Builder to, BooleanQuery from) { flattenBooleanQuery(to, from, 1f); } private static void flattenBooleanQuery(BooleanQuery.Builder to, BooleanQuery from, float fromBoost) { for (BooleanClause clause : from.clauses()) { Query cq = clause.getQuery(); float boost = fromBoost; while (cq instanceof BoostQuery) { BoostQuery bq = (BoostQuery) cq; cq = bq.getQuery(); boost *= bq.getBoost(); } if (cq instanceof BooleanQuery && !clause.isRequired() && !clause.isProhibited()) { /* we can recurse */ flattenBooleanQuery(to, (BooleanQuery)cq, boost); } else { to.add(clause); } } } /** * Escapes all special characters except '"', '-', and '+' */ public static CharSequence partialEscape(CharSequence s) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '\\' || c == '!' || c == '(' || c == ')' || c == ':' || c == '^' || c == '[' || c == ']' || c == '/' || c == '{' || c == '}' || c == '~' || c == '*' || c == '?' ) { sb.append('\\'); } sb.append(c); } return sb; } // Pattern to detect dangling operator(s) at end of query // \s+[-+\s]+$ private final static Pattern DANGLING_OP_PATTERN = Pattern.compile( "\\s+[-+\\s]+$" ); // Pattern to detect consecutive + and/or - operators // \s+[+-](?:\s*[+-]+)+ private final static Pattern CONSECUTIVE_OP_PATTERN = Pattern.compile( "\\s+[+-](?:\\s*[+-]+)+" ); protected static final String UNKNOWN_VALUE = "Unknown"; /** * Strips operators that are used illegally, otherwise returns its * input. Some examples of illegal user queries are: "chocolate +- * chip", "chocolate - - chip", and "chocolate chip -". */ public static CharSequence stripIllegalOperators(CharSequence s) { String temp = CONSECUTIVE_OP_PATTERN.matcher( s ).replaceAll( " " ); return DANGLING_OP_PATTERN.matcher( temp ).replaceAll( "" ); } /** * Returns its input if there is an even (ie: balanced) number of * '"' characters -- otherwise returns a String in which all '"' * characters are striped out. */ public static CharSequence stripUnbalancedQuotes(CharSequence s) { int count = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '\"') { count++; } } if (0 == (count & 1)) { return s; } return s.toString().replace("\"",""); } /** * Adds to {@code dest} all the not-null elements of {@code entries} that have non-null names * * @param entries The array of entries to be added to the {@link NamedList} {@code dest} * @param dest The {@link NamedList} instance where the not-null elements of entries are added * @return Returns The {@code dest} input object */ public static <T> NamedList<T> removeNulls(Map.Entry<String, T>[] entries, NamedList<T> dest) { for (int i=0; i<entries.length; i++) { Map.Entry<String, T> entry = entries[i]; if (entry != null) { String key = entry.getKey(); if (key != null) { dest.add(key, entry.getValue()); } } } return dest; } /** Copies the given {@code namedList} assumed to have doc uniqueKey keyed data into {@code destArr} * at the position of the document in the response. destArr is assumed to be the same size as * {@code resultIds} is. {@code resultIds} comes from {@link ResponseBuilder#resultIds}. If the doc key * isn't in {@code resultIds} then it is ignored. * Note: most likely you will call {@link #removeNulls(Map.Entry[], NamedList)} sometime after calling this. */ public static void copyNamedListIntoArrayByDocPosInResponse(NamedList namedList, Map<Object, ShardDoc> resultIds, Map.Entry<String, Object>[] destArr) { assert resultIds.size() == destArr.length; for (int i = 0; i < namedList.size(); i++) { String id = namedList.getName(i); // TODO: lookup won't work for non-string ids... String vs Float ShardDoc sdoc = resultIds.get(id); if (sdoc != null) { // maybe null when rb.onePassDistributedQuery int idx = sdoc.positionInResponse; destArr[idx] = new NamedList.NamedListEntry<>(id, namedList.getVal(i)); } } } /** * A subclass of SolrQueryParser that supports aliasing fields for * constructing DisjunctionMaxQueries. */ public static class DisjunctionMaxQueryParser extends SolrQueryParser { /** A simple container for storing alias info * @see #aliases */ protected static class Alias { public float tie; public Map<String,Float> fields; } /** * Where we store a map from field name we expect to see in our query * string, to Alias object containing the fields to use in our * DisjunctionMaxQuery and the tiebreaker to use. */ protected Map<String,Alias> aliases = new HashMap<>(3); public DisjunctionMaxQueryParser(QParser qp, String defaultField) { super(qp,defaultField); // don't trust that our parent class won't ever change its default setDefaultOperator(QueryParser.Operator.OR); } /** * Add an alias to this query parser. * * @param field the field name that should trigger alias mapping * @param fieldBoosts the mapping from fieldname to boost value that * should be used to build up the clauses of the * DisjunctionMaxQuery. * @param tiebreaker to the tiebreaker to be used in the * DisjunctionMaxQuery * @see SolrPluginUtils#parseFieldBoosts */ public void addAlias(String field, float tiebreaker, Map<String,Float> fieldBoosts) { Alias a = new Alias(); a.tie = tiebreaker; a.fields = fieldBoosts; aliases.put(field, a); } /** * Delegates to the super class unless the field has been specified * as an alias -- in which case we recurse on each of * the aliased fields, and the results are composed into a * DisjunctionMaxQuery. (so yes: aliases which point at other * aliases should work) */ @Override protected Query getFieldQuery(String field, String queryText, boolean quoted, boolean raw) throws SyntaxError { if (aliases.containsKey(field)) { Alias a = aliases.get(field); List<Query> disjuncts = new ArrayList<>(); for (String f : a.fields.keySet()) { Query sub = getFieldQuery(f,queryText,quoted, false); if (null != sub) { if (null != a.fields.get(f)) { sub = new BoostQuery(sub, a.fields.get(f)); } disjuncts.add(sub); } } return disjuncts.isEmpty() ? null : new DisjunctionMaxQuery(disjuncts, a.tie); } else { try { return super.getFieldQuery(field, queryText, quoted, raw); } catch (Exception e) { return null; } } } } /** * Determines the correct Sort based on the request parameter "sort" * * @return null if no sort is specified. */ public static Sort getSort(SolrQueryRequest req) { String sort = req.getParams().get(CommonParams.SORT); if (null == sort || sort.equals("")) { return null; } SolrException sortE = null; Sort ss = null; try { ss = SortSpecParsing.parseSortSpec(sort, req).getSort(); } catch (SolrException e) { sortE = e; } if ((null == ss) || (null != sortE)) { /* we definitely had some sort of sort string from the user, * but no SortSpec came out of it */ log.warn("Invalid sort \""+sort+"\" was specified, ignoring", sortE); return null; } return ss; } /** Turns an array of query strings into a List of Query objects. * * @return null if no queries are generated */ public static List<Query> parseQueryStrings(SolrQueryRequest req, String[] queries) throws SyntaxError { if (null == queries || 0 == queries.length) return null; List<Query> out = new ArrayList<>(queries.length); for (String q : queries) { if (null != q && 0 != q.trim().length()) { out.add(QParser.getParser(q, req).getQuery()); } } return out; } /** * A CacheRegenerator that can be used whenever the items in the cache * are not dependant on the current searcher. * * <p> * Flat out copies the oldKey=>oldVal pair into the newCache * </p> */ public static class IdentityRegenerator implements CacheRegenerator { @Override public boolean regenerateItem(SolrIndexSearcher newSearcher, SolrCache newCache, SolrCache oldCache, Object oldKey, Object oldVal) throws IOException { newCache.put(oldKey,oldVal); return true; } } /** * Convert a DocList to a SolrDocumentList * * The optional param "ids" is populated with the lucene document id * for each SolrDocument. * * @param docs The {@link org.apache.solr.search.DocList} to convert * @param searcher The {@link org.apache.solr.search.SolrIndexSearcher} to use to load the docs from the Lucene index * @param fields The names of the Fields to load * @param ids A map to store the ids of the docs * @return The new {@link org.apache.solr.common.SolrDocumentList} containing all the loaded docs * @throws java.io.IOException if there was a problem loading the docs * @since solr 1.4 * @deprecated TODO in 7.0 remove this. It was inlined into ClusteringComponent. DWS: 'ids' is ugly. */ public static SolrDocumentList docListToSolrDocumentList( DocList docs, SolrIndexSearcher searcher, Set<String> fields, Map<SolrDocument, Integer> ids ) throws IOException { /* DWS deprecation note: It's only called by ClusteringComponent, and I think the "ids" param aspect is a bit messy and not worth supporting. If someone wants a similar method they can speak up and we can add a method to SolrDocumentFetcher. */ IndexSchema schema = searcher.getSchema(); SolrDocumentList list = new SolrDocumentList(); list.setNumFound(docs.matches()); list.setMaxScore(docs.maxScore()); list.setStart(docs.offset()); DocIterator dit = docs.iterator(); while (dit.hasNext()) { int docid = dit.nextDoc(); Document luceneDoc = searcher.doc(docid, fields); SolrDocument doc = new SolrDocument(); for( IndexableField field : luceneDoc) { if (null == fields || fields.contains(field.name())) { SchemaField sf = schema.getField( field.name() ); doc.addField( field.name(), sf.getType().toObject( field ) ); } } if (docs.hasScores() && (null == fields || fields.contains("score"))) { doc.addField("score", dit.score()); } list.add( doc ); if( ids != null ) { ids.put( doc, new Integer(docid) ); } } return list; } public static void invokeSetters(Object bean, Iterable<Map.Entry<String,Object>> initArgs) { invokeSetters(bean, initArgs, false); } public static void invokeSetters(Object bean, Iterable<Map.Entry<String,Object>> initArgs, boolean lenient) { if (initArgs == null) return; final Class<?> clazz = bean.getClass(); for (Map.Entry<String,Object> entry : initArgs) { String key = entry.getKey(); String setterName = "set" + String.valueOf(Character.toUpperCase(key.charAt(0))) + key.substring(1); try { final Object val = entry.getValue(); final Method method = findSetter(clazz, setterName, key, val.getClass(), lenient); if (method != null) { method.invoke(bean, val); } } catch (InvocationTargetException | IllegalAccessException e1) { if (lenient) { continue; } throw new RuntimeException("Error invoking setter " + setterName + " on class : " + clazz.getName(), e1); } } } private static Method findSetter(Class<?> clazz, String setterName, String key, Class<?> paramClazz, boolean lenient) { BeanInfo beanInfo; try { beanInfo = Introspector.getBeanInfo(clazz); } catch (IntrospectionException ie) { if (lenient) { return null; } throw new RuntimeException("Error getting bean info for class : " + clazz.getName(), ie); } for (final boolean matchParamClazz: new boolean[]{true, false}) { for (final MethodDescriptor desc : beanInfo.getMethodDescriptors()) { final Method m = desc.getMethod(); final Class<?> p[] = m.getParameterTypes(); if (m.getName().equals(setterName) && p.length == 1 && (!matchParamClazz || paramClazz.equals(p[0]))) { return m; } } } if (lenient) { return null; } throw new RuntimeException("No setter corrresponding to '" + key + "' in " + clazz.getName()); } /** * Given the integer purpose of a request generates a readable value corresponding * the request purposes (there can be more than one on a single request). If * there is a purpose parameter present that's not known this method will * return {@value #UNKNOWN_VALUE} * @param reqPurpose Numeric request purpose * @return a comma separated list of purposes or {@value #UNKNOWN_VALUE} */ public static String getRequestPurpose(Integer reqPurpose) { if (reqPurpose != null) { StringBuilder builder = new StringBuilder(); for (Map.Entry<Integer, String>entry : purposes.entrySet()) { if ((reqPurpose & entry.getKey()) != 0) { builder.append(entry.getValue() + ","); } } if (builder.length() == 0) { return UNKNOWN_VALUE; } builder.setLength(builder.length() - 1); return builder.toString(); } return UNKNOWN_VALUE; } }