Configuring Funnelback for instant search

Background

This article details the steps involved in configuring Funnelback to provide an instant search. Instant search is where the search results display updates as you type the query.

See an example of a working Funnelback instant search: instant search showcase demo

Process

  1. Create a pre process hook script to expand query:

    hook_pre_process.groovy
    // Add partial query support to padre
    // reads the query CGI parameter and creates a disjunctive query based on the suggestions returned from padre-qs, injected into the system query parameter
    
    // Imports required for access to the padre suggest service
    import java.io.File;
    import java.util.List;
    import com.funnelback.dataapi.connector.padre.PadreConnector;
    import com.funnelback.dataapi.connector.padre.suggest.Suggestion;
    import com.funnelback.dataapi.connector.padre.suggest.Suggestion.ActionType;
    import com.funnelback.dataapi.connector.padre.suggest.Suggestion.DisplayType;
    // Imports required to enable logging
    //import org.apache.logging.log4j.LogManager;
    //import org.apache.logging.log4j.Logger;
    //Logger logger = LogManager.getLogger("partial query expander")
    
    def q = transaction.question
    if (q.collection.configuration.value(["partial_query_enabled"])) {
        // Convert a partial query into a set of query terms
        // Maximum number of query terms to expand partial query to - read from collection.cfg partial_query_expansion_index parameter.
        // eg. partial_query=com might expand to query=[commerce commercial common computing]
        def partial_query_expansion_index = 5
        if ((q.collection.configuration.value(["partial_query_expansion_index"]) != null) && (q.collection.configuration.value(["partial_query_expansion_index"]).isInteger())) {
        	partial_query_expansion_index = q.collection.configuration.value(["partial_query_expansion_index"])
        }
    
        if (q.query != null) {
            File searchHome = new File("/opt/funnelback")
            File indexStem = new File(q.collection.configuration.value(["collection_root"]) + File.separator + "live" + File.separator + "idx","index")
    
            // NOTE: CONSTRUCTOR HAS CHANGED post v15.16 and requires 3 parameters
            List<Suggestion> suggestions = new PadreConnector(searchHome,indexStem,q.collection.id)
              .suggest(q.query)
              .suggestionCount(partial_query_expansion_index)
              .fetch();
    
            // Use this for v15.0-15.14
            /* List<Suggestion> suggestions = new PadreConnector(searchHome,indexStem)
              .suggest(q.query)
              .suggestionCount(partial_query_expansion_index)
              .fetch(); */
    
    		// Use this instead for v14.2 and earlier
            /* List<Suggestion> suggestions = new PadreConnector(indexStem)
              .suggest(q.query)
              .suggestionCount(partial_query_expansion_index)
              .fetch(); */
    
            // build the expanded query from the list of suggestions
            def expanded_query = ""
            suggestions.each {
                    expanded_query += '"'+it.key+'" '
            }
    
            // set the query to the expanded set of query terms ORed together
            if (expanded_query != "") {
                            q.additionalParameters["s"] = ["["+expanded_query+"]"]
                    }
        }
    }
  2. Add collection configuration options:

    collection.cfg
    # Option to enable the hook script code above
    partial_query_enabled=true
    # optional parameter to control number of suggestions to expand the query to (def = 5)
    #partial_query_expansion_index=5
    # disable query completion
    query_completion_enabled=false
  3. Add the following javascript to the web resources folder ($SEARCH_HOME/conf/$COLLECTION_NAME/_default/web/jquery.fb.instantSearch.js).

    jquery.fb.instantSearch.js
    (function($) {
        $.widget('fb.instantSearch', {
            options: {
                autocomplete : false,
                form         : 'results-instant',
                length       : 3,
                updatedSel   : '#search-results-instant',
            },
            autocomplete: function(val) { // turn on|off autocomplete dropdown
                if (typeof(val) != 'undefined') this.options.autocomplete = val;
                if (this.element.is(':ui-autocomplete')) this.element.autocomplete(this.options.autocomplete ? 'enable' : 'disable');
                return this.options.autocomplete;
            },
            callUpdate: function() { // call ajax request and update chunk of page based on return html response
                var that = this, form = that.form();
                $.get(form.attr('action'), that._setParameter(form.serialize(), 'form', that.options.form)).done(function(data) {
                    $(that.options.updatedSel).html(data);
                    var url = that._setParameter(document.location.search, 'query', that.element.val());
                    window.history.pushState({html:data, pageTitle:that.element.val()}, '', url); // update query parameter in browser address bar URL
                });
            },
            form: function() { // get parent form of element
                return this.element.closest('form');
            },
            _create: function() {
                if (!$(this.options.updatedSel).length) return;
                var that = this;
                that.autocomplete();
                that.element.keyup(function(e) {
                    if ($(this).val().length < that.options.length) return;
                    that.callUpdate();
                });
            },
            _setParameter: function(str, key, val) { // add or update parameter
                var regex = new RegExp('([?&]' + key + ')=([^#&]*)', 'g');
                return str.match(regex) ? str.replace(regex, '$1=' + val) : str + '&' + key + '=' + val;
            }
        });
    })(jQuery);
  4. Modify the search template to call the instant search function from the in-page Javascript block (eg. after query completion code). Note: the length parameter controls the minimum length of the partial query used to trigger the instant search.

    Freemarker template (e.g. simple.ftl)
    // Instant search
    jQuery("input.query").instantSearch({
        length : '<@s.cfg>query_completion.length</@s.cfg>'
    });
  5. Include a #search-results-instant div in the html page code - note this should be included even in the s.InitialFormOnly section.

    Freemarker template (e.g. simple.ftl)
    <@s.InitialFormOnly>
    <div id="search-results-instant" />
    </@s.InitialFormOnly>
    
    <@s.AfterSearchOnly>
    <div id="search-results-instant" class="row" data-ng-show="isDisplayed('results')">
    ...
    </div>
    </@s.AfterSearchOnly>