no.sesat.search.mode.command.AbstractFast4SearchCommand.java Source code

Java tutorial

Introduction

Here is the source code for no.sesat.search.mode.command.AbstractFast4SearchCommand.java

Source

/* Copyright (2005-2012) Schibsted ASA
 * This file is part of Possom.
 *
 *   Possom is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Lesser General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   Possom is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public License
 *   along with Possom.  If not, see <http://www.gnu.org/licenses/>.
 *
 * AbstractFast4SearchCommand.java
 *
 * Created on 14 March 2006, 19:51
 *
 */

package no.sesat.search.mode.command;

import no.fast.ds.search.BaseParameter;
import no.fast.ds.search.ConfigurationException;
import no.fast.ds.search.FastSearchEngineFactory;
import no.fast.ds.search.IDocumentSummary;
import no.fast.ds.search.IDocumentSummaryField;
import no.fast.ds.search.IFastSearchEngine;
import no.fast.ds.search.IFastSearchEngineFactory;
import no.fast.ds.search.IModifier;
import no.fast.ds.search.INavigator;
import no.fast.ds.search.IQuery;
import no.fast.ds.search.IQueryResult;
import no.fast.ds.search.IQueryTransformation;
import no.fast.ds.search.IQueryTransformations;
import no.fast.ds.search.ISearchParameters;
import no.fast.ds.search.Query;
import no.fast.ds.search.SearchEngineException;
import no.fast.ds.search.SearchParameter;
import no.fast.ds.search.SearchParameters;
import no.sesat.search.datamodel.generic.StringDataObject;
import no.sesat.search.mode.config.FastCommandConfig;
import no.sesat.search.result.BasicResultItem;
import no.sesat.search.result.FastSearchResult;
import no.sesat.search.result.Modifier;
import no.sesat.search.result.Navigator;
import no.sesat.search.site.config.SiteConfiguration;
import no.sesat.search.result.BasicWeightedSuggestion;
import no.sesat.search.result.ModifierDateComparator;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import no.sesat.search.result.ResultItem;
import no.sesat.search.result.ResultList;
import no.sesat.search.result.WeightedSuggestion;

/**
 * Handles the basic implementation of the FAST 4 search.
 *
 * Includes support for
 *  - faceted search (modifiers or navigators),
 *  - relevant queries, proper name and spelling suggestions.
 *
 * @version $Id$
 */
public abstract class AbstractFast4SearchCommand extends AbstractSearchCommand {

    // Constants -----------------------------------------------------
    private static final Logger LOG = Logger.getLogger(AbstractFast4SearchCommand.class);
    private static final String ERR_FAST_FAILURE = " suffered from a FAST error ";
    private static final String ERR_EXECUTE_FAILURE = "execute() failed";
    private static final String DEBUG_FAST_SEARCH_ENGINE = "Creating Fast Engine to ";
    private static final String DEBUG_EXECUTE_QR_URL = "execute() QueryServerURL=";
    private static final String DEBUG_EXECUTE_COLLECTIONS = "execute() Collections=";
    private static final String DEBUG_EXECUTE_QUERY = "execute() Query=";
    private static final String DEBUG_EXECUTE_FILTER = "execute() Filter=";
    private static final String COLLAPSE_PARAMETER = "collapse";

    // Attributes ----------------------------------------------------
    private final Map<String, Navigator> navigators;
    // Deprecated code. I cannot see how anybody is using it. Much related below is also commented out.
    //    private final Map<String, String[]> navigatedValues = new HashMap<String, String[]>();

    private final String queryServerUrl;

    // Static --------------------------------------------------------
    private static final Map<String, IFastSearchEngine> SEARCH_ENGINES = new ConcurrentHashMap<String, IFastSearchEngine>();

    private static transient IFastSearchEngineFactory engineFactory;

    static {
        try {
            engineFactory = FastSearchEngineFactory.newInstance();

        } catch (ConfigurationException e) {
            LOG.fatal(e.getMessage(), e);
            // Who exactly is expected to catch from a static constructor?
            throw new SearchCommandException(e);
        }
    }

    // Constructors --------------------------------------------------

    /**
     * Creates a new instance of AbstractFast4SearchCommand
     * @param cxt the context the search command must work within.
     */
    public AbstractFast4SearchCommand(final Context cxt) {

        super(cxt);

        final FastCommandConfig conf = (FastCommandConfig) cxt.getSearchConfiguration();
        final SiteConfiguration siteConf = cxt.getDataModel().getSite().getSiteConfiguration();
        queryServerUrl = siteConf.getProperty(conf.getQueryServerUrl());
        navigators = conf.getNavigators();
    }

    // Public --------------------------------------------------------

    /** Return all our configured navigator's field:value pairs in one string in filter syntax.
     * Commas in the navigator's parameter value are treated as separators between optional selected navigator items.
     *
     * @deprecated @todo extract to a query transformer, FastNavigationQueryTransformer
     *
     * @return field:value filter string.
     */
    public Collection<String> createNavigationFilterStrings() {

        Collection<String> filterStrings = new ArrayList<String>();

        //        for (final Iterator iterator = navigatedValues.keySet().iterator(); iterator.hasNext();) {
        //            final String field = (String) iterator.next();
        //
        //            final String modifiers[] = navigatedValues.get(field);
        //
        //            for (int i = 0; i < modifiers.length; i++) {
        //                if (!field.equals("contentsource") || !modifiers[i].equals("Norske nyheter")) {
        //                    if ("adv".equals(getSearchConfiguration().getFiltertype()))
        //                        filterStrings.add(" AND " + field + ":\"" + modifiers[i] + "\"");
        //                    else
        //                        filterStrings.add("+" + field + ":\"" + modifiers[i] + "\"");
        //                }
        //            }
        //        }

        for (final Navigator navigator : getNavigators().values()) {

            filterStrings = createNavigationFilterStrings(filterStrings, navigator);
        }

        return filterStrings;
    }

    /**
     * set the IFastSearchEngineFactory to use when getting, or creating, IFastSearchEngines.
     * @param factory
     */
    public static void setSearchEngineFactory(final IFastSearchEngineFactory factory) {
        engineFactory = factory;
    }

    // Z implementation ----------------------------------------------

    // SearchCommand overrides ----------------------------------------------

    /**
     * Assured associated search configuration will always be of this type. *
     */
    @Override
    public FastCommandConfig getSearchConfiguration() {
        return (FastCommandConfig) super.getSearchConfiguration();
    }

    public ResultList<ResultItem> execute() {

        try {
            final IFastSearchEngine engine = getSearchEngine();

            final IQuery fastQuery = createQuery();

            IQueryResult result = null;
            try {

                LOG.debug(DEBUG_EXECUTE_QR_URL + queryServerUrl);
                LOG.debug(DEBUG_EXECUTE_COLLECTIONS + getSearchConfiguration().getCollections());
                LOG.debug(DEBUG_EXECUTE_QUERY + fastQuery.getQueryString());
                LOG.debug(DEBUG_EXECUTE_FILTER + getSearchConfiguration().getCollectionFilterString());

                result = engine.search(fastQuery);

            } catch (SocketTimeoutException ste) {

                LOG.error(getSearchConfiguration().getName() + " --> " + ste.getMessage());
                return new FastSearchResult<ResultItem>();

            } catch (IOException ioe) {

                LOG.error(getSearchConfiguration().getName() + ERR_FAST_FAILURE, ioe);
                return new FastSearchResult<ResultItem>();

            } catch (SearchEngineException fe) {

                LOG.error(getSearchConfiguration().getName() + ERR_FAST_FAILURE + '[' + fe.getErrorCode() + ']',
                        fe);
                return new FastSearchResult<ResultItem>();

            }

            DUMP.info(fastQuery.toString());

            final FastSearchResult<ResultItem> searchResult = collectResults(result);

            if (getSearchConfiguration().isSpellcheck()) {
                collectSpellingSuggestions(result, searchResult);
            }

            if (getSearchConfiguration().isRelevantQueries() && null == getParameter("qs")) {
                collectRelevantQueries(result, searchResult);
            }

            if (getNavigators() != null) {
                collectModifiers(result, searchResult);
            }

            final String collapseId = getParameter(COLLAPSE_PARAMETER);

            if (getSearchConfiguration().isCollapsing() && getSearchConfiguration().isExpansion()) {

                if (collapseId != null && !collapseId.equals("")) {

                    if (searchResult.getResults().size() > 0) {
                        final ResultItem itm = searchResult.getResults().get(0);
                        final URL url = new URL(itm.getField("url"));
                        searchResult.addField("collapsedDomain", url.getHost());
                    }
                }
            }

            return searchResult;

        } catch (ConfigurationException e) {
            LOG.error(ERR_EXECUTE_FAILURE, e);
            throw new SearchCommandException(e);

        } catch (MalformedURLException e) {
            LOG.error(ERR_EXECUTE_FAILURE, e);
            throw new SearchCommandException(e);
        }
    }

    /**
     * @deprecated does nothing. here only to maintain API.
     */
    public void addNavigatedTo(final String navigatorKey) {
    }

    // Package protected ---------------------------------------------

    // Protected -----------------------------------------------------

    /**
     * Get the navigators from the command's SearchConfiguration.
     * @return the navigators
     */
    protected Map<String, Navigator> getNavigators() {

        return navigators;
    }

    @Override
    protected int getResultsToReturn() {

        return getSearchConfiguration().getResultsToReturn();
    }

    /**
     * Get, or create, the IFastSearchEngine responsible for connecting to and fetching the results from the fsat index.
     * @return the IFastSearchEngine
     * @throws ConfigurationException
     * @throws MalformedURLException index url is malformed
     */
    protected IFastSearchEngine getSearchEngine() throws ConfigurationException, MalformedURLException {
        try {
            if (!SEARCH_ENGINES.containsKey(queryServerUrl)) {
                LOG.debug(DEBUG_FAST_SEARCH_ENGINE + getSearchConfiguration().getQueryServerUrl() + "-->"
                        + queryServerUrl);

                final IFastSearchEngine engine = engineFactory.createSearchEngine(queryServerUrl);
                SEARCH_ENGINES.put(queryServerUrl, engine);
            }
            return SEARCH_ENGINES.get(queryServerUrl);
        } catch (MalformedURLException e) {
            LOG.error("Malformed URL is: " + queryServerUrl);
            throw (e);
        }
    }

    protected String getSortBy() {

        return getSearchConfiguration().getSortBy();
    }

    /** Add additional fast search parameters.
     * iSearchParameters.setParameter(new SearchParameter(...));
     *
     * @param iSearchParameters live list of parameters being built and eventually used in the fast query.
     */
    protected void setAdditionalParameters(final ISearchParameters iSearchParameters) {

        for (Map.Entry<String, String> entry : getSearchConfiguration().getSearchParameterMap().entrySet()) {
            iSearchParameters.setParameter(new SearchParameter(entry.getKey(), entry.getValue()));
        }
    }

    // Private -------------------------------------------------------

    @SuppressWarnings("unchecked")
    private void collectSpellingSuggestions(final IQueryResult result, final FastSearchResult searchResult) {

        final IQueryTransformations qTransforms = result.getQueryTransformations(false);
        if (qTransforms.getSuggestions().size() > 0) {
            for (IQueryTransformation transformation : (Collection<IQueryTransformation>) qTransforms
                    .getAllQueryTransformations()) {

                if (transformation.getName().equals("FastQT_SpellCheck")
                        && transformation.getAction().equals("nop")) {
                    final String custom = transformation.getCustom();
                    final WeightedSuggestion suggestion = createSpellingSuggestion(custom);
                    searchResult.addSpellingSuggestion(suggestion);
                }

                if (transformation.getName().equals("FastQT_ProperName")) {
                    String custom = transformation.getCustom();
                    WeightedSuggestion suggestion = createProperNameSuggestion(custom);
                    if (suggestion != null) {
                        searchResult.addSpellingSuggestion(suggestion);
                    }
                }
            }
        }

        if ("42".equals(datamodel.getQuery().getString())) {
            final WeightedSuggestion egg = BasicWeightedSuggestion.instanceOf("42", "Meningen med livet",
                    "Meningen med livet", 1000);
            searchResult.addSpellingSuggestion(egg);
        }

        if ("kvasir".equalsIgnoreCase(datamodel.getQuery().getString())) {
            final WeightedSuggestion egg = BasicWeightedSuggestion.instanceOf("kvasir", "sesam", "sesam", 1000);
            searchResult.addSpellingSuggestion(egg);
        }

        if ("meningen med livet".equalsIgnoreCase(datamodel.getQuery().getString())) {
            final WeightedSuggestion egg = BasicWeightedSuggestion.instanceOf("meningen med livet", "42", "42",
                    1000);
            searchResult.addSpellingSuggestion(egg);
        }
    }

    private WeightedSuggestion createSpellingSuggestion(final String custom) {

        final int suggestionIndex = custom.indexOf("->");
        final int qualityIndex = custom.indexOf("Quality:");

        LOG.debug("Custom is " + custom);

        final String orig = custom.substring(0, suggestionIndex);
        final String string = custom.substring(suggestionIndex + 2, qualityIndex - 2);
        final String quality = custom.substring(qualityIndex + 9, qualityIndex + 12);

        return BasicWeightedSuggestion.instanceOf(orig, string, string, Integer.parseInt(quality));
    }

    private FastSearchResult<ResultItem> collectResults(final IQueryResult result) {

        if (LOG.isDebugEnabled()) {

            LOG.debug(
                    getSearchConfiguration().getName() + " Collecting results. There are " + result.getDocCount());
            LOG.debug(getSearchConfiguration().getName() + " Number of results to collect: "
                    + getSearchConfiguration().getResultsToReturn());

        }

        final FastSearchResult<ResultItem> searchResult = new FastSearchResult<ResultItem>();
        final int cnt = getOffset();

        final int maxIndex = Math.min(cnt + getResultsToReturn(), result.getDocCount());

        searchResult.setHitCount(result.getDocCount());

        for (int i = cnt; i < maxIndex; i++) {
            final IDocumentSummary document = result.getDocument(i + 1);
            //catch nullpointerException because of unaccurate doccount
            try {
                final ResultItem item = createResultItem(document);
                searchResult.addResult(item);
            } catch (NullPointerException e) {
                LOG.debug("Error finding document", e);
                return searchResult;
            }
        }
        return searchResult;
    }

    private ResultItem createResultItem(final IDocumentSummary document) {

        ResultItem item = new BasicResultItem();

        for (final Map.Entry<String, String> entry : getSearchConfiguration().getResultFieldMap().entrySet()) {
            final IDocumentSummaryField summary = document.getSummaryField(entry.getKey());

            if (summary != null) {
                item = item.addField(entry.getValue(), summary.getSummary());
            }
        }

        if (getSearchConfiguration().isCollapsing() && getSearchConfiguration().isExpansion()) {
            final String currCollapseId = getParameter(COLLAPSE_PARAMETER);

            if (currCollapseId == null || currCollapseId.equals("")) {
                final String moreHits = document.getSummaryField("morehits").getSummary();

                if (moreHits.equals("1")) {
                    item = item.addField("moreHits", "true").addField("collapseParameter", COLLAPSE_PARAMETER)
                            .addField("collapseId", document.getSummaryField("collapseid").getSummary());
                }
            }
        }

        return item;
    }

    private IQuery createQuery() {

        final ISearchParameters params = new SearchParameters();
        params.setParameter(new SearchParameter(BaseParameter.LEMMATIZE, getSearchConfiguration().isLemmatise()));

        params.setParameter(
                new SearchParameter("sesat:uniqueId", context.getDataModel().getParameters().getUniqueId()));

        if (getSearchConfiguration().isSpellcheck()) {
            params.setParameter(new SearchParameter(BaseParameter.SPELL, "suggest"));
            params.setParameter(new SearchParameter("qtf_spellcheck:addconsidered", "1"));
            params.setParameter(new SearchParameter("qtf_spellcheck:consideredverbose", "1"));
        }

        if (getSearchConfiguration().getName() != null
                && getSearchConfiguration().getName().equals("relevantQueries")) {
            params.setParameter(new SearchParameter("sources", "alone"));
        }

        String kwString = "";
        String queryString = getTransformedQuery();

        if (getSearchConfiguration().isKeywordClusteringEnabled()) {
            if (null != getParameter("kw")) {
                kwString = getParameter("kw");
            }

            if (!kwString.equals("")) {
                queryString += " " + kwString;
            }
        }
        // TODO: This is a little bit messy
        // Set filter, the filtertype may be adv
        final StringBuilder filter = new StringBuilder(getSearchConfiguration().getCollectionFilterString());

        if (!getSearchConfiguration().isIgnoreNavigation() && getNavigators() != null) {

            final Collection<String> navStrings = createNavigationFilterStrings();
            filter.append(' ');
            filter.append(' ').append(StringUtils.join(navStrings.iterator(), " "));
        }

        if (getSearchConfiguration().getOffensiveScoreLimit() > 0) {
            filter.append(' ').append("-ocfscore:>").append(getSearchConfiguration().getOffensiveScoreLimit());

        }

        if (getSearchConfiguration().getSpamScoreLimit() > 0) {
            filter.append(' ').append("+spamscore:<").append(getSearchConfiguration().getSpamScoreLimit());
        }

        final String collapseId = getParameter(COLLAPSE_PARAMETER);

        if (getSearchConfiguration().isCollapsing()) {
            if (null == collapseId || "".equals(collapseId) || !getSearchConfiguration().isExpansion()) {
                params.setParameter(new SearchParameter(BaseParameter.COLLAPSING, true));

            } else {
                params.setParameter(new SearchParameter(BaseParameter.COLLAPSING, false));
                filter.append(" +collapseid:").append(collapseId);
            }
        }

        if (getSearchConfiguration().getFilter() != null && getSearchConfiguration().getFilter().length() > 0) {
            // TODO create NowMacroFilterQueryTransfomer to do this instead
            final Calendar c = Calendar.getInstance();
            final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            final String updatedFilter = getSearchConfiguration().getFilter().replaceAll("\\{NOW\\}",
                    sdf.format(c.getTime()));

            filter.append(' ' + updatedFilter);
        }

        // Init dynamic filters
        final String superFilter = null == getFilter() ? "" : getFilter();

        LOG.debug("createQuery: superFilter=" + superFilter);

        params.setParameter(new SearchParameter("filtertype",
                "adv".equals(getSearchConfiguration().getFiltertype()) ? "adv" : "any"));

        params.setParameter(new SearchParameter(BaseParameter.TYPE, getSearchConfiguration().getQueryType()));

        params.setParameter(new SearchParameter(BaseParameter.FILTER, filter.toString() + ' ' + superFilter));

        if (getSearchConfiguration().getQtPipeline() != null
                && getSearchConfiguration().getQtPipeline().length() > 0) {
            params.setParameter(
                    new SearchParameter(BaseParameter.QTPIPELINE, getSearchConfiguration().getQtPipeline()));
        }
        params.setParameter(new SearchParameter(BaseParameter.QUERY, queryString));
        params.setParameter(new SearchParameter(BaseParameter.COLLAPSING, getSearchConfiguration().isCollapsing()));
        params.setParameter(
                new SearchParameter(BaseParameter.LANGUAGE, getSearchConfiguration().getSpellchecklanguage()));

        if (getNavigators() != null && getNavigators().size() > 0) {
            params.setParameter(new SearchParameter(BaseParameter.NAVIGATION, true));
        }

        params.setParameter(new SearchParameter("hits", getResultsToReturn()));
        params.setParameter(new SearchParameter(BaseParameter.CLUSTERING, getSearchConfiguration().isClustering()));

        if (getSearchConfiguration().getResultView() != null
                && getSearchConfiguration().getResultView().length() > 0) {
            params.setParameter(
                    new SearchParameter(BaseParameter.RESULT_VIEW, getSearchConfiguration().getResultView()));

        }

        // default SORT_BY is defined by configuration
        if (getSortBy() != null && getSortBy().length() > 0) {
            params.setParameter(new SearchParameter(BaseParameter.SORT_BY, getSortBy()));
        }

        // XXX Move-out. This code is specific to Norway's mobileYellowGeo mode!
        if (null != getParameter("c") && getParameter("c").equals("yg")) {
            if (getParameter("type").equals("f")) {
                params.setParameter(new SearchParameter("qtf_geosearch:center",
                        '(' + getParameter("cla") + ',' + getParameter("clo")));

                params.setParameter(new SearchParameter("qtf_geosearch:filterbox", "[(" + getParameter("la1") + ","
                        + getParameter("lo1") + ");(" + getParameter("la2") + ',' + getParameter("lo2") + ")]"));

                params.setParameter(new SearchParameter("sortdirection", "ascending"));

            } else {
                params.setParameter(new SearchParameter("qtf_geosearch:center",
                        '(' + getParameter("cla") + "," + getParameter("clo")));

                params.setParameter(new SearchParameter("qtf_geosearch:radius", getParameter("rad")));
                params.setParameter(new SearchParameter("sortdirection", "ascending"));
            }
        } // <-- END-OF move-out

        if (null != getParameter("rank")) {
            params.setParameter(new SearchParameter(BaseParameter.SORT_BY, getParameter("rank")));
        }

        // This  now uses sort order from canfiguration, old ones still here for backwards compability untill all are uppdated
        if (isUserSortable()) {

            final String sortBy = getUserSortBy();
            LOG.debug("userSortBy " + sortBy);

            // TODO move-out to genericno. this is configuration hardcoded.
            if ("default".equals(sortBy)) {
                params.setParameter(
                        new SearchParameter(BaseParameter.SORT_BY, getSearchConfiguration().getSortBy()));

            } else if ("alternative".equals(sortBy) && getSearchConfiguration().getAlternativeSortBy() != null) {
                params.setParameter(new SearchParameter(BaseParameter.SORT_BY,
                        getSearchConfiguration().getAlternativeSortBy()));

            } else if ("standard".equals(sortBy)) {
                params.setParameter(new SearchParameter(BaseParameter.SORT_BY, "retriever"));

            } else if ("datetime".equals(sortBy)) {
                params.setParameter(new SearchParameter(BaseParameter.SORT_BY, "docdatetime+standard"));

            } else if ("-datetime".equals(sortBy)) {
                params.setParameter(new SearchParameter(BaseParameter.SORT_BY, "+docdatetime"));

            } // <-- END-OF move-out.
        }

        params.setParameter(new SearchParameter(BaseParameter.NAVIGATORS, getNavigatorsString()));

        setAdditionalParameters(params);

        return new Query(params);
    }

    private String getNavigatorsString() {

        final Collection<Navigator> allFlattened = new ArrayList<Navigator>();

        for (Navigator navigator : getNavigators().values()) {

            allFlattened.addAll(flattenNavigators(new ArrayList<Navigator>(), navigator));
        }

        return StringUtils.join(allFlattened.iterator(), ',');
    }

    private Collection<Navigator> flattenNavigators(Collection<Navigator> soFar, Navigator nav) {

        soFar.add(nav);

        // don't ask for navigators one step beyond anything not selected
        if (null != nav.getChildNavigator() && null != datamodel.getParameters().getValue(nav.getId())) {
            flattenNavigators(soFar, nav.getChildNavigator());
        }

        return soFar;
    }

    private void collectModifiers(IQueryResult result, FastSearchResult<ResultItem> searchResult) {

        for (Navigator nav : getNavigators().values()) {

            collectModifier(nav, result, searchResult);
        }
    }

    private void collectModifier(final Navigator nav, final IQueryResult result,
            final FastSearchResult<ResultItem> searchResult) {

        final String navigatorKey = nav.getId();

        final INavigator navigator = result.getNavigator(nav.getName());

        if (null != navigator) {

            final Iterator iterator = navigator.modifiers();

            while (iterator.hasNext()) {

                final IModifier modifier = (IModifier) iterator.next();

                if (!"unknown".equals(modifier.getName())) {

                    searchResult.addModifier(navigatorKey,
                            new Modifier(modifier.getName(), modifier.getCount(), nav));
                }
            }

            // live copy of list!
            final List<Modifier> modifiers = searchResult.getModifiers(navigatorKey);

            if (null != modifiers) {
                switch (nav.getSort()) {
                case DAY_MONTH_YEAR:
                    Collections.sort(modifiers, ModifierDateComparator.DAY_MONTH_YEAR);
                    break;
                case DAY_MONTH_YEAR_DESCENDING:
                    Collections.sort(modifiers, ModifierDateComparator.DAY_MONTH_YEAR_DESCENDING);
                    break;
                case YEAR_MONTH_DAY_DESCENDING:
                    Collections.sort(modifiers, ModifierDateComparator.YEAR_MONTH_DAY_DESCENDING);
                    break;
                case YEAR:
                    Collections.sort(modifiers, ModifierDateComparator.YEAR);
                    break;
                case MONTH_YEAR:
                    Collections.sort(modifiers, ModifierDateComparator.MONTH_YEAR);
                    break;
                case CUSTOM:
                    Collections.sort(modifiers, getModifierComparator(nav));
                    break;
                case NONE:
                    // Use the sorting the index returns
                    break;
                case COUNT:
                    /* Fall through */
                default:
                    Collections.sort(modifiers);
                    break;
                }
            }

            // include any child
            if (null != nav.getChildNavigator()) {
                collectModifier(nav.getChildNavigator(), result, searchResult);
            }
        }
    }

    /** Override to provide custom modifier comparators.
     * This implementation always return null.
     *
     * @param nav
     * @return null
     */
    protected Comparator<Modifier> getModifierComparator(final Navigator nav) {
        return null;
    }

    private WeightedSuggestion createProperNameSuggestion(String custom) {

        int suggestionIndex = custom.indexOf("->");

        LOG.debug("Custom is " + custom);

        String orig = custom.substring(0, suggestionIndex);
        String string = custom.substring(suggestionIndex + 2);

        string = string.replaceAll("\"", "");

        if (orig.equalsIgnoreCase(string)) {
            return null;
        }

        return BasicWeightedSuggestion.instanceOf(orig, string, string, 1000);
    }

    @SuppressWarnings("unchecked")
    private void collectRelevantQueries(IQueryResult result, FastSearchResult searchResult) {

        if (result.getQueryTransformations(false).getSuggestions().size() > 0) {

            for (IQueryTransformation transformation : (Collection<IQueryTransformation>) result
                    .getQueryTransformations(false).getAllQueryTransformations()) {

                if (transformation.getName().equals("FastQT_Synonym") && transformation.getMessageID() == 8) {

                    final String query = transformation.getQuery();
                    final String[] forWords = query.split("#!#");

                    for (int i = 0; i < forWords.length; i++) {

                        final String[] forOneWord = forWords[i].split("###");

                        for (int j = 0; j < forOneWord.length; j++) {

                            final String[] suggNweight = forOneWord[j].split("@");

                            if (!datamodel.getQuery().getString().equalsIgnoreCase(suggNweight[0])) {

                                final WeightedSuggestion rq = BasicWeightedSuggestion.instanceOf(
                                        getQuery().getQueryString(), suggNweight[0], suggNweight[0],
                                        2 == suggNweight.length ? Integer.valueOf(suggNweight[1])
                                                : Integer.MIN_VALUE);

                                searchResult.addRelevantQuery(rq);
                            }
                        }
                    }
                }
            }
        }
    }

    private Collection<String> createNavigationFilterStrings(final Collection<String> filterStrings,
            final Navigator navigator) {

        final StringDataObject navigatedValue = datamodel.getParameters().getValue(navigator.getId());

        if (null != navigatedValue) {

            // TODO this test should be encapsulated with the delegated filterBuilder
            final StringBuilder filter = new StringBuilder(
                    "adv".equals(getSearchConfiguration().getFiltertype()) ? " AND (" : " +(");

            // splitting here allows for multiple navigation selections within the one navigation level.
            for (String navSingleValue : navigatedValue.getString().split(",")) {

                final String value = navigator.isBoundaryMatch() ? "^\"" + navSingleValue + "\"$"
                        : "\"" + navSingleValue + "\"";

                filter.append(' ' + navigator.getField() + ':' + value);
            }

            filterStrings.add(filter.append(" )").toString());
        }

        // include any children (if parent is selected)
        return null != navigatedValue && null != navigator.getChildNavigator()
                ? createNavigationFilterStrings(filterStrings, navigator.getChildNavigator())
                : filterStrings;
    }

    // Inner classes -------------------------------------------------

}