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
-
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+"]"] } } }
-
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
-
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);
-
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>' });
-
Include a
#search-results-instant
div in the html page code - note this should be included even in thes.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>