blob: 5096308cc17781041f530ef493813e7c8050b241 [file] [log] [blame]
/*
* 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.analysis.Analyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.simple.SimpleQueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BoostQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.SimpleParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.parser.QueryParser;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.TextField;
import org.apache.solr.util.SolrPluginUtils;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* Create a query from the input value that will be parsed by Lucene's SimpleQueryParser.
* See {@link org.apache.lucene.queryparser.simple.SimpleQueryParser} for details on the exact syntax allowed
* to be used for queries.
* <br>
* The following options may be applied for parsing the query.
* <ul>
* <li>
* q.operators - Used to enable specific operations for parsing. The operations that can be enabled are
* and, not, or, prefix, phrase, precedence, escape, and whitespace. By default all operations
* are enabled. All operations can be disabled by passing in an empty string to this parameter.
* </li>
* <li>
* q.op - Used to specify the operator to be used if whitespace is a delimiter. Either 'AND' or 'OR'
* can be specified for this parameter. Any other string will cause an exception to be thrown.
* If this parameter is not specified 'OR' will be used by default.
* </li>
* <li>
* qf - The list of query fields and boosts to use when building the simple query. The format is the following:
* <code>fieldA^1.0 fieldB^2.2</code>. A field can also be specified without a boost by simply listing the
* field as <code>fieldA fieldB</code>. Any field without a boost will default to use a boost of 1.0.
* </li>
* <li>
* df - An override for the default field specified in the schema or a default field if one is not specified
* in the schema. If qf is not specified the default field will be used as the field to run the query
* against.
* </li>
* </ul>
*/
public class SimpleQParserPlugin extends QParserPlugin {
/** The name that can be used to specify this plugin should be used to parse the query. */
public static final String NAME = "simple";
/** Map of string operators to their int counterparts in SimpleQueryParser. */
private static final Map<String, Integer> OPERATORS = new HashMap<>();
/* Setup the map of possible operators. */
static {
OPERATORS.put(SimpleParams.AND_OPERATOR, SimpleQueryParser.AND_OPERATOR);
OPERATORS.put(SimpleParams.NOT_OPERATOR, SimpleQueryParser.NOT_OPERATOR);
OPERATORS.put(SimpleParams.OR_OPERATOR, SimpleQueryParser.OR_OPERATOR);
OPERATORS.put(SimpleParams.PREFIX_OPERATOR, SimpleQueryParser.PREFIX_OPERATOR);
OPERATORS.put(SimpleParams.PHRASE_OPERATOR, SimpleQueryParser.PHRASE_OPERATOR);
OPERATORS.put(SimpleParams.PRECEDENCE_OPERATORS, SimpleQueryParser.PRECEDENCE_OPERATORS);
OPERATORS.put(SimpleParams.ESCAPE_OPERATOR, SimpleQueryParser.ESCAPE_OPERATOR);
OPERATORS.put(SimpleParams.WHITESPACE_OPERATOR, SimpleQueryParser.WHITESPACE_OPERATOR);
OPERATORS.put(SimpleParams.FUZZY_OPERATOR, SimpleQueryParser.FUZZY_OPERATOR);
OPERATORS.put(SimpleParams.NEAR_OPERATOR, SimpleQueryParser.NEAR_OPERATOR);
}
/** Returns a QParser that will create a query by using Lucene's SimpleQueryParser. */
@Override
public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
return new SimpleQParser(qstr, localParams, params, req);
}
private static class SimpleQParser extends QParser {
private SimpleQueryParser parser;
public SimpleQParser (String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
super(qstr, localParams, params, req);
// Some of the parameters may come in through localParams, so combine them with params.
SolrParams defaultParams = SolrParams.wrapDefaults(localParams, params);
// This will be used to specify what fields and boosts will be used by SimpleQueryParser.
Map<String, Float> queryFields = SolrPluginUtils.parseFieldBoosts(defaultParams.get(SimpleParams.QF));
if (queryFields.isEmpty()) {
// It qf is not specified setup up the queryFields map to use the defaultField.
String defaultField = defaultParams.get(CommonParams.DF);
if (defaultField == null) {
// A query cannot be run without having a field or set of fields to run against.
throw new IllegalStateException("Neither " + SimpleParams.QF + " nor " + CommonParams.DF
+ " are present.");
}
queryFields.put(defaultField, 1.0F);
}
else {
for (Map.Entry<String, Float> queryField : queryFields.entrySet()) {
if (queryField.getValue() == null) {
// Some fields may be specified without a boost, so default the boost to 1.0 since a null value
// will not be accepted by SimpleQueryParser.
queryField.setValue(1.0F);
}
}
}
// Setup the operations that are enabled for the query.
int enabledOps = 0;
String opParam = defaultParams.get(SimpleParams.QO);
if (opParam == null) {
// All operations will be enabled.
enabledOps = -1;
} else {
// Parse the specified enabled operations to be used by the query.
String[] operations = opParam.split(",");
for (String operation : operations) {
Integer enabledOp = OPERATORS.get(operation.trim().toUpperCase(Locale.ROOT));
if (enabledOp != null) {
enabledOps |= enabledOp;
}
}
}
// Create a SimpleQueryParser using the analyzer from the schema.
final IndexSchema schema = req.getSchema();
parser = new SolrSimpleQueryParser(req.getSchema().getQueryAnalyzer(), queryFields, enabledOps, this, schema);
// Set the default operator to be either 'AND' or 'OR' for the query.
QueryParser.Operator defaultOp = QueryParsing.parseOP(defaultParams.get(QueryParsing.OP));
if (defaultOp == QueryParser.Operator.AND) {
parser.setDefaultOperator(BooleanClause.Occur.MUST);
}
}
@Override
public Query parse() throws SyntaxError {
return parser.parse(qstr);
}
}
private static class SolrSimpleQueryParser extends SimpleQueryParser {
QParser qParser;
IndexSchema schema;
public SolrSimpleQueryParser(Analyzer analyzer, Map<String, Float> weights, int flags,
QParser qParser, IndexSchema schema) {
super(analyzer, weights, flags);
this.qParser = qParser;
this.schema = schema;
}
@Override
protected Query newPrefixQuery(String text) {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
for (Map.Entry<String, Float> entry : weights.entrySet()) {
String field = entry.getKey();
FieldType type = schema.getFieldType(field);
Query prefix = null;
if (type instanceof TextField) {
// If the field type is a TextField then use the multi term analyzer.
Analyzer analyzer = ((TextField)type).getMultiTermAnalyzer();
BytesRef termBytes = TextField.analyzeMultiTerm(field, text, analyzer);
if (termBytes != null) {
String term = termBytes.utf8ToString();
SchemaField sf = schema.getField(field);
prefix = sf.getType().getPrefixQuery(qParser, sf, term);
}
} else {
// If the type is *not* a TextField don't do any analysis.
SchemaField sf = schema.getField(field);
prefix = type.getPrefixQuery(qParser, sf, text);
}
if (prefix != null) {
float boost = entry.getValue();
if (boost != 1f) {
prefix = new BoostQuery(prefix, boost);
}
bq.add(prefix, BooleanClause.Occur.SHOULD);
}
}
return simplify(bq.build());
}
@Override
protected Query newFuzzyQuery(String text, int fuzziness) {
BooleanQuery.Builder bq = new BooleanQuery.Builder();
for (Map.Entry<String, Float> entry : weights.entrySet()) {
String field = entry.getKey();
FieldType type = schema.getFieldType(field);
Query fuzzy = null;
if (type instanceof TextField) {
// If the field type is a TextField then use the multi term analyzer.
Analyzer analyzer = ((TextField)type).getMultiTermAnalyzer();
BytesRef termBytes = TextField.analyzeMultiTerm(field, text, analyzer);
if (termBytes != null) {
String term = termBytes.utf8ToString();
fuzzy = new FuzzyQuery(new Term(entry.getKey(), term), fuzziness);
}
} else {
// If the type is *not* a TextField don't do any analysis.
fuzzy = new FuzzyQuery(new Term(entry.getKey(), text), fuzziness);
}
if (fuzzy != null) {
float boost = entry.getValue();
if (boost != 1f) {
fuzzy = new BoostQuery(fuzzy, boost);
}
bq.add(fuzzy, BooleanClause.Occur.SHOULD);
}
}
return simplify(bq.build());
}
}
}