com.sindicetech.siren.solr.qparser.SirenQParser.java Source code

Java tutorial

Introduction

Here is the source code for com.sindicetech.siren.solr.qparser.SirenQParser.java

Source

/**
 * Copyright (c) 2014, Sindice Limited. All Rights Reserved.
 *
 * This file is part of the SIREn project.
 *
 * 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 com.sindicetech.siren.solr.qparser;

import com.sindicetech.siren.search.node.NodeBooleanQuery;
import com.sindicetech.siren.search.node.TwigQuery;
import com.sindicetech.siren.solr.schema.Datatype;
import com.sindicetech.siren.solr.schema.ExtendedJsonField;
import org.apache.lucene.analysis.Analyzer;
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.search.SyntaxError;
import org.apache.solr.util.SolrPluginUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

/**
 * 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 boolean allowLeadingWildcard;
    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);

        // change the BooleanQuery maxClauseCount for ALL cores ...
        // Since QParserPlugin cannot be SolrCoreAware, we cannot do this at plugin init time
        int maxClauseCount = req.getCore().getSolrConfig().booleanQueryMaxClauseCount;
        NodeBooleanQuery.setMaxClauseCount(maxClauseCount);
        TwigQuery.setMaxClauseCount(maxClauseCount);
    }

    /**
     * Set the QNames mapping for use in the query parser.
     */
    public void setQNames(final Properties qnames) {
        this.qnames = qnames;
    }

    /**
     * Enable or disable leading wildcard
     */
    public void setAllowLeadingWildcard(final boolean allowLeadingWildcard) {
        this.allowLeadingWildcard = allowLeadingWildcard;
    }

    @Override
    public Query parse() throws SyntaxError {
        final SolrParams solrParams = SolrParams.wrapDefaults(localParams, params);
        final Map<String, Float> boosts = parseQueryFields(req.getSchema(), solrParams);

        // We disable the coord because this query is an artificial construct
        final BooleanQuery query = new BooleanQuery(true);
        // if empty main query, ignore and try to parse the nested queries
        if (qstr != null && !qstr.isEmpty()) {
            this.processMainQuery(query, boosts, qstr);
        }
        this.processNestedQuery(query, solrParams);

        return query;
    }

    /**
     * Process the main query, and add it to the {@link org.apache.lucene.search.BooleanQuery} that will be executed.
     * Perform the expansion to multiple fields if necessary by creating a
     * {@link org.apache.lucene.search.BooleanClause.Occur#SHOULD} clause for each field query.
     */
    private void processMainQuery(BooleanQuery query, final Map<String, Float> boosts, final String qstr)
            throws SyntaxError {
        BooleanQuery bq = new BooleanQuery(true); // combine the main query for each field in a nested boolean query
        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));
            }
            bq.add(q, Occur.SHOULD);
        }
        query.add(bq, Occur.MUST); // add the nested boolean query to the main query with the MUST operator - See issue #60
    }

    /**
     * Process the nested queries and add them as a (MUST) clause of the {@link org.apache.lucene.search.BooleanQuery}
     * that will be executed.
     */
    private void processNestedQuery(final BooleanQuery main, final SolrParams solrParams) throws SyntaxError {
        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 SyntaxError;

    /**
     * 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 SyntaxError {
        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 ExtendedJsonField fieldType = (ExtendedJsonField) 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 SyntaxError {
        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 SyntaxError("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 com.sindicetech.siren.solr.schema.ExtendedJsonField}.
     */
    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 ExtendedJsonField)) {
                throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "FieldType: " + fieldName + " ("
                        + fieldType.getTypeName() + ") do not support SIREn's tree query");
            }
        }
    }

}