Package grails.plugin.searchable.internal.compass.search

Source Code of grails.plugin.searchable.internal.compass.search.DefaultSuggestQueryMethod

/*
* Copyright 2007 the original author or authors.
*
* 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 grails.plugin.searchable.internal.compass.search;

import grails.plugin.searchable.internal.compass.mapping.CompassMappingUtils;
import grails.plugin.searchable.internal.compass.support.AbstractSearchableMethod;
import grails.plugin.searchable.internal.compass.support.SearchableMethodUtils;
import grails.plugin.searchable.internal.lucene.LuceneUtils;
import groovy.lang.Closure;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.compass.core.Compass;
import org.compass.core.CompassCallback;
import org.compass.core.CompassException;
import org.compass.core.CompassQuery;
import org.compass.core.CompassSession;
import org.compass.core.engine.SearchEngineQueryParseException;
import org.springframework.util.Assert;

/**
* @author Maurice Nicholson
*/
public class DefaultSuggestQueryMethod extends AbstractSearchableMethod {

    private SearchableCompassQueryBuilder compassQueryBuilder;
    private GrailsApplication grailsApplication;

    public DefaultSuggestQueryMethod(String methodName, Compass compass, GrailsApplication grailsApplication) {
        this(methodName, compass, grailsApplication, new HashMap());
    }

    public DefaultSuggestQueryMethod(String methodName, Compass compass, GrailsApplication grailsApplication, Map defaultOptions) {
        super(methodName, compass, null, defaultOptions);
        this.grailsApplication = grailsApplication;
    }

    public Object invoke(Object[] args) {
        if (!CompassMappingUtils.hasSpellCheckMapping(getCompass())) {
            throw new IllegalStateException(
                "Suggestions are only available when classes are mapped with \"spellCheck\" options, either at the class " +
                "or property level. The simplest way to do this is add spellCheck \"include\" to the domain class searchable mapping closure. " +
                "See the plugin/Compass documentation Mapping sections for details."
            );
        }
        if (!"true".equals(getCompass().getSettings().getSetting("compass.engine.spellcheck.enable"))) {
            throw new IllegalStateException(
                "Suggestions are only available when the Compass Spell Check feature is enabled, but currently it is not. " +
                "Please set Compass setting 'compass.engine.spellcheck.enable' to 'true'. " +
                "One way to so this is to use the SearchableConfiguration.groovy file (run \"grails install-searchable-config\") and " +
                "add \"'compass.engine.spellcheck.enable': 'true'\" to the compassSettings Map. " +
                "Also see the Spell Check section in the Compass docs for additional settings."
            );
        }

        Object query = SearchableMethodUtils.getQueryArgument(args);
        if (query instanceof Closure) {
            throw new UnsupportedOperationException("Closure queries are not support for query suggestions, only String queries.");
        }
        Assert.isInstanceOf(String.class, query, "Only String queries are supported for query suggestions");

        SuggestQueryCompassCallback suggestQueryCallback = new SuggestQueryCompassCallback(getCompass(), getDefaultOptions(), args);
        Map options = getOptions(args);
        suggestQueryCallback.applyOptions(options);
        suggestQueryCallback.setGrailsApplication(grailsApplication);
        suggestQueryCallback.setCompassQueryBuilder(compassQueryBuilder);
        return doInCompass(suggestQueryCallback);
    }

    public SearchableCompassQueryBuilder getCompassQueryBuilder() {
        return compassQueryBuilder;
    }

    public void setCompassQueryBuilder(SearchableCompassQueryBuilder compassQueryBuilder) {
        this.compassQueryBuilder = compassQueryBuilder;
    }

    public Map getOptions(Object[] args) {
        Map options = new HashMap(getDefaultOptions()); // clone to avoid corrupting original
        options.putAll(SearchableMethodUtils.getOptionsArgument(args, null));
        return options;
    }

    public static class SuggestQueryCompassCallback implements CompassCallback {
        private Map defaultOptions;
        private Object[] args;
        private SearchableCompassQueryBuilder compassQueryBuilder;
        private GrailsApplication grailsApplication;
        private boolean userFriendly;
        private boolean emulateCapitalisation;
        private boolean escape;
        private boolean allowSame;

        public SuggestQueryCompassCallback(Compass compass, Map defaultOptions, Object[] args) {
            this.defaultOptions = defaultOptions;
            this.args = args;
        }

        public Object doInCompass(CompassSession session) throws CompassException {
            Map options = SearchableMethodUtils.getOptionsArgument(args, defaultOptions);
            options.put("analyzer", "searchableplugin_simple");
            CompassQuery original = compassQueryBuilder.buildQuery(grailsApplication, session, options, args);
            String queryString = original.getSuggestedQuery().toString();
            String suggestedString = queryString;
            if (options.containsKey("class")) {
                // Strip the additional junk from around the query - +(what test) +(alias:B)
                Pattern pattern = Pattern.compile("\\+\\((.+)\\) \\+\\(alias:.+\\)");
                Matcher matcher = pattern.matcher(queryString);
                if (!matcher.matches()) {
                    return queryString;
                }
                suggestedString = matcher.group(1);
            }
            String originalString = (String) SearchableMethodUtils.getQueryArgument(args);
            try {
                return new SuggestedQueryStringBuilder(originalString, suggestedString).
                    userFriendly(userFriendly).
                    emulateCapitalisation(emulateCapitalisation).
                    escape(escape).
                    allowSame(allowSame).
                    toSuggestedQueryString();
            } catch (ParseException ex) {
                throw new SearchEngineQueryParseException(
                    "Failed to parse one of the queries: orignal [" + originalString + "], suggested: [" + suggestedString + "]",
                    ex
                );
            }
        }

        public void setCompassQueryBuilder(SearchableCompassQueryBuilder compassQueryBuilder) {
            this.compassQueryBuilder = compassQueryBuilder;
        }

        public void setGrailsApplication(GrailsApplication grailsApplication) {
            this.grailsApplication = grailsApplication;
        }

        public void applyOptions(Map options) {
            if (options == null) {
                return;
            }
            userFriendly = SearchableMethodUtils.getBool(options, "userFriendly", true);
            emulateCapitalisation = SearchableMethodUtils.getBool(options, "emulateCapitalisation", true);
            escape = SearchableMethodUtils.getBool(options, "escape", false);
            allowSame = SearchableMethodUtils.getBool(options, "allowSame", true);
        }
    }

    public static class SuggestedQueryStringBuilder {
        private static final Log LOG = LogFactory.getLog(SuggestedQueryStringBuilder.class);
        private static final String defaultField = "$SuggestedQueryStringUtils_defaultField$";

        private String original;
        private String suggested;
        private boolean userFriendly = true;
        private boolean emulateCapitalisation = true;
        private boolean escape = false;
        private boolean allowSame = true;

        /**
         * Create a suggested query string builder with the given original and suggested query strings
         * @param original the original query - probably from a user
         * @param suggested the suggested query - probably from the Compass suggestion engine
         */
        public SuggestedQueryStringBuilder(String original, String suggested) {
            this.original = original;
            this.suggested = suggested;
        }

        /**
         * Enable/disable whether queries suggested by {@link #toSuggestedQueryString} are user-frienly,
         * ie, look like the user's original query.
         * This feature is enabled by default
         * If you disable this feature, the emulateCapitalisation setting is ignored and the suggested query
         * is returned by {@link # toSuggestedQueryString}  as-is
         * @param userFriendly true or false to enable or disable
         * @return this
         */
        public SuggestedQueryStringBuilder userFriendly(boolean userFriendly) {
            this.userFriendly = userFriendly;
            return this;
        }

        /**
         * Enable/disable the emulation of capitalised words.
         * This feature is enabled by default
         * @param emulateCapitalisation true or false to enable or disable
         * @return this
         */
        public SuggestedQueryStringBuilder emulateCapitalisation(boolean emulateCapitalisation) {
            this.emulateCapitalisation = emulateCapitalisation;
            return this;
        }

        /**
         * Enable/disable whether to allow the same query to be suggested as the original
         * This is enabled by default
         * @param allowSame true or false to enable or disable
         * @return this
         */
        public SuggestedQueryStringBuilder allowSame(boolean allowSame) {
            this.allowSame = allowSame;
            return this;
        }

        /**
         * Get the suggested query based on the options set
         * @return the suggested query string or null if allowSame is false and the queries match
         * @throws ParseException if either original or suggested query is invalid
         */
        public String toSuggestedQueryString() throws ParseException {
            if (!userFriendly) {
                return suggested;
            }

            Term[] originalTerms = LuceneUtils.realTermsForQueryString(defaultField, escape ? LuceneUtils.cleanQuery(original) : original, WhitespaceAnalyzer.class);
            Term[] suggestedTerms = LuceneUtils.realTermsForQueryString(defaultField, suggested, WhitespaceAnalyzer.class);

            if (originalTerms.length != suggestedTerms.length) {
                LOG.warn(
                    "Expected the same number of terms for original query [" + original + "] and suggested query [" + suggested + "], " +
                    "but original query has [" + originalTerms.length + "] terms and suggested query has [" + suggestedTerms.length + "] terms " +
                    "so unable to provide user friendly version. Returning suggested query as-is."
                );
                return suggested;
            }

            StringBuilder userFriendly = new StringBuilder(original);
            int offset = 0;
            for (int i = 0; i < originalTerms.length; i++) {
                Term originalTerm = originalTerms[i];
                boolean noField = originalTerm.field().equals(defaultField);
                String snippet = noField ? originalTerm.text() : originalTerm.field() + ":" + originalTerm.text();
                int pos = userFriendly.indexOf(snippet, offset);
                Term suggestedTerm = suggestedTerms[i];
                String replacement = getReplacement(originalTerm, noField, suggestedTerm);
                userFriendly.replace(pos, pos + snippet.length(), replacement);
                offset = pos;
            }
            String suggestion = userFriendly.toString();
            if (!allowSame && suggestion.equals(original)) {
                return null;
            }
            return suggestion;
        }

        public SuggestedQueryStringBuilder escape(boolean escape) {
            this.escape = escape;
            return this;
        }

        private String getReplacement(Term originalTerm, boolean noField, Term suggestedTerm) {
            String replacement = noField ? suggestedTerm.text() : originalTerm.field() + ":" + suggestedTerm.text();
            if (emulateCapitalisation) {
                boolean upperCase = true;
                boolean firstUpperCase = false;
                final String original = originalTerm.text();
                for (int i = 0; i < original.length(); i++) {
                    if (!Character.isUpperCase(original.charAt(i))) {
                        upperCase = false;
                        break;
                    }
                    if (i == 0) {
                        firstUpperCase = true;
                    }
                }
                if (upperCase) {
                    return replacement.toUpperCase();
                }
                if (firstUpperCase) {
                    return replacement.substring(0, 1).toUpperCase() + (replacement.length() > 1 ? replacement.substring(1) : "");
                }
            }
            return replacement;
        }
    }
}
TOP

Related Classes of grails.plugin.searchable.internal.compass.search.DefaultSuggestQueryMethod

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.