/** * Copyright 2014 National University of Ireland, Galway. * * This file is part of the SIREn project. Project and contact information: * * https://github.com/rdelbru/SIREn * * Licensed 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.sindice.siren.solr.qparser; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.flexible.standard.config.StandardQueryConfigHandler.Operator; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.schema.FieldType; import org.apache.solr.schema.IndexSchema; import org.apache.solr.search.QParser; import org.apache.solr.search.QueryParsing; import org.apache.solr.util.SolrPluginUtils; import org.sindice.siren.solr.schema.Datatype; import org.sindice.siren.solr.schema.SirenField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link SirenQParser} is in charge of parsing a SIREn query request. * <p> * Expand the query to multiple fields by constructing a disjunction of the * parsed query across the fields. * <p> * For each <code>nested</code> parameter in the request, its argument * is parsed as a subquery and added to the main query. * <p> * The default operator for use by the query parsers is {@link Operator#AND}. It * can be overwritten using the parameter {@link QueryParsing#OP}. */ public abstract class SirenQParser extends QParser { protected Properties qnames; private static final Logger logger = LoggerFactory.getLogger(SirenQParser.class); public SirenQParser(final String qstr, final SolrParams localParams, final SolrParams params, final SolrQueryRequest req) { super(qstr, localParams, params, req); } /** * Set the QNames mapping for use in the query parser. */ public void setQNames(final Properties qnames) { this.qnames = qnames; } @Override public Query parse() throws ParseException { if (qstr == null || qstr.length()==0) return null; final SolrParams solrParams = SolrParams.wrapDefaults(localParams, params); final Map<String, Float> boosts = parseQueryFields(req.getSchema(), solrParams); final BooleanQuery main = this.getMainQuery(boosts, qstr); this.addNestedQuery(main, solrParams); return main; } /** * Build the main query that will be executed. Expand to multiple fields if * necessary. * @throws ParseException */ private BooleanQuery getMainQuery(final Map<String, Float> boosts, final String qstr) throws ParseException { // We disable the coord because this query is an artificial construct final BooleanQuery query = new BooleanQuery(true); for (final String field : boosts.keySet()) { final Map<String, Analyzer> datatypeConfig = this.getDatatypeConfig(field); final Query q = this.parse(field, qstr, datatypeConfig); if (boosts.get(field) != null) { q.setBoost(boosts.get(field)); } query.add(q, Occur.SHOULD); } return query; } /** * Build the nested queries and add them as a (MUST) clause of the main query. */ private void addNestedQuery(final BooleanQuery main, final SolrParams solrParams) throws ParseException { if (solrParams.getParams("nested") != null) { for (final String nested : solrParams.getParams("nested")) { final QParser baseParser = this.subQuery(nested, null); main.add(baseParser.getQuery(), Occur.MUST); } } } protected abstract Query parse(final String field, final String qstr, final Map<String, Analyzer> datatypeConfig) throws ParseException; /** * Create a new QParser for parsing an embedded nested query. * <p> * Remove the nested parameters from the original request to avoid infinite * recursion. */ @Override public QParser subQuery(final String q, final String defaultType) throws ParseException { final QParser nestedParser = super.subQuery(q, defaultType); final NamedList<Object> params = nestedParser.getParams().toNamedList(); params.remove("nested"); nestedParser.setParams(SolrParams.toSolrParams(params)); return nestedParser; } /** * Retrieve the datatype query analyzers associated to this field */ private Map<String, Analyzer> getDatatypeConfig(final String field) { final Map<String, Analyzer> datatypeConfig = new HashMap<String, Analyzer>(); final SirenField fieldType = (SirenField) req.getSchema().getFieldType(field); final Map<String, Datatype> datatypes = fieldType.getDatatypes(); for (final Entry<String, Datatype> e : datatypes.entrySet()) { if (e.getValue().getQueryAnalyzer() == null) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Configuration Error: No analyzer defined for type 'query' in " + "datatype " + e.getKey()); } datatypeConfig.put(e.getKey(), e.getValue().getQueryAnalyzer()); } return datatypeConfig; } protected Operator getDefaultOperator() { final String val = params.get(QueryParsing.OP); Operator defaultOp = Operator.AND; // default AND operator if (val != null) { defaultOp = "AND".equals(val) ? Operator.AND : Operator.OR; } return defaultOp; } /** * Uses {@link SolrPluginUtils#parseFieldBoosts(String)} with the 'qf' * parameter. Falls back to the 'df' parameter or * {@link org.apache.solr.schema.IndexSchema#getDefaultSearchFieldName()}. */ public static Map<String, Float> parseQueryFields(final IndexSchema indexSchema, final SolrParams solrParams) throws ParseException { final Map<String, Float> queryFields = SolrPluginUtils.parseFieldBoosts(solrParams.getParams(SirenParams.QF)); if (queryFields.isEmpty()) { final String df = QueryParsing.getDefaultField(indexSchema, solrParams.get(CommonParams.DF)); if (df == null) { throw new ParseException("Neither "+SirenParams.QF+", "+CommonParams.DF +", nor the default search field are present."); } queryFields.put(df, 1.0f); } checkFieldTypes(indexSchema, queryFields); return queryFields; } /** * Check if all fields are of type {@link SirenField}. */ private static void checkFieldTypes(final IndexSchema indexSchema, final Map<String, Float> queryFields) { for (final String fieldName : queryFields.keySet()) { final FieldType fieldType = indexSchema.getFieldType(fieldName); if (!(fieldType instanceof SirenField)) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "FieldType: " + fieldName + " (" + fieldType.getTypeName() + ") do not support Siren's tree query"); } } } }