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

org.apache.solr.util.SolrPluginUtils Maven / Gradle / Ivy

There is a newer version: 9.6.1
Show newest version
/*
 * 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.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.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;

/**
 * Utilities that may be of use to RequestHandlers.
 */
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 purposes;

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  static {
      Map 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.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 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:
   *
   * 
    *
  • Locates the document-retrieval costs in one spot, which helps * detailed performance measurement
  • * *
  • 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).
  • *
* * 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 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 getDebugInterests(String[] params, ResponseBuilder rb){ Set 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; } /** *

* Returns a NamedList containing many "standard" pieces of debugging * information. *

* *
    *
  • rawquerystring - the 'q' param exactly as specified by the client *
  • *
  • querystring - the 'q' param after any preprocessing done by the plugin *
  • *
  • parsedquery - the main query executed formated by the Solr * QueryParsing utils class (which knows about field types) *
  • *
  • parsedquery_toString - the main query executed formatted by its * own toString method (in case it has internal state Solr * doesn't know about) *
  • *
  • explain - the list of score explanations for each document in * results against query. *
  • *
  • otherQuery - the query string specified in 'explainOther' query param. *
  • *
  • explainOther - the list of score explanations for each document in * results against 'otherQuery' *
  • *
* * @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 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 explainO = getExplanations(query, otherResults, searcher, schema); dbg.add("explainOther", explainStruct ? explanationsToNamedLists(explainO) : explanationsToStrings(explainO)); } } } public static NamedList explanationToNamedList(Explanation e) { NamedList 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> kids = new ArrayList<>(details.length); for (Explanation d : details) { kids.add(explanationToNamedList(d)); } out.add("details", kids); return out; } public static NamedList> explanationsToNamedLists (NamedList explanations) { NamedList> out = new SimpleOrderedMap<>(); for (Map.Entry 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 getExplanations (Query query, DocList docs, SolrIndexSearcher searcher, IndexSchema schema) throws IOException { NamedList explainList = new SimpleOrderedMap<>(); DocIterator iterator = docs.iterator(); for (int i=0; i explanationsToStrings (NamedList explanations) { NamedList out = new SimpleOrderedMap<>(); for (Map.Entry 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 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. * *

* Doesn't care if boost info is negative, you're on your own. *

*

* Doesn't care if boost info is missing, again: you're on your own. *

* * @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 parseFieldBoosts(String in) { return parseFieldBoosts(new String[]{in}); } /** * Like parseFieldBoosts(String), but parses all the strings * in the provided array (which may be null). * * @param fieldLists an array of Strings eg. {"fieldOne^2.3", "fieldTwo", fieldThree^-0.4} * @return Map of fieldOne => 2.3, fieldTwo => null, fieldThree => -0.4 */ public static Map parseFieldBoosts(String[] fieldLists) { if (null == fieldLists || 0 == fieldLists.length) { return new HashMap<>(); } Map 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. {"fieldOne^2.3", "fieldTwo", fieldThree~5^-0.4} * @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 parseFieldBoostsAndSlop(String[] fieldLists,int wordGrams,int defaultSlop) { if (null == fieldLists || 0 == fieldLists.length) { return new ArrayList<>(); } List 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. *

* 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. *

*

* Details about the specification format can be found * here *

* *

A few important notes...

*
    *
  • * 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) *
  • *
  • * 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) *
  • *
* *

:TODO: should optimize the case where number is same * as clauses to just make them all "required" *

* * @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. * *

* 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. *

*/ 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 NamedList removeNulls(Map.Entry[] entries, NamedList dest) { for (int i=0; i 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 resultIds, Map.Entry[] 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 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 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 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 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 parseQueryStrings(SolrQueryRequest req, String[] queries) throws SyntaxError { if (null == queries || 0 == queries.length) return null; List 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. * *

* Flat out copies the oldKey=>oldVal pair into the newCache *

*/ 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; } } public static void invokeSetters(Object bean, Iterable> initArgs) { invokeSetters(bean, initArgs, false); } public static void invokeSetters(Object bean, Iterable> initArgs, boolean lenient) { if (initArgs == null) return; final Class clazz = bean.getClass(); for (Map.Entry 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); } catch (AssertionError ae) { throw new RuntimeException("Error invoking setter " + setterName + " on class : " + clazz.getName()+ ". This might be a case of SOLR-12207", ae); } } } 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.Entryentry : 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; } private static final String[] purposeUnknown = new String[] { UNKNOWN_VALUE }; /** * 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 a 1-element array containing {@value #UNKNOWN_VALUE} * @param reqPurpose Numeric request purpose * @return an array of purpose names. */ public static String[] getRequestPurposeNames(Integer reqPurpose) { if (reqPurpose != null) { int valid = 0; for (Map.Entryentry : purposes.entrySet()) { if ((reqPurpose & entry.getKey()) != 0) { valid++; } } if (valid == 0) { return purposeUnknown; } else { String[] result = new String[valid]; int i = 0; for (Map.Entryentry : purposes.entrySet()) { if ((reqPurpose & entry.getKey()) != 0) { result[i] = entry.getValue(); i++; } } return result; } } return purposeUnknown; } }