Hook scripts

This feature is deprecated and unavailable to users of the Squiz Experience Cloud version of Funnelback. Equivalent functionality is available using plugins.

Search lifecycle hook scripts can be used to implement custom code that manipulates the search data model when a search is run.

There are four hook scripts corresponding to the pre/post phases of the search lifecycle:

  • hook_pre_process.groovy: This runs after initial question object population, but before any of the input processing occurs. Manipulation of the query and addition or modification of most question attributes can be made at this point.

  • hook_pre_datafetch.groovy: This runs after all of the input processing is complete, but just before the query is submitted. This hook can be used to manipulate any additional data model elements that are populated by the input processing. This is most commonly used for modifying faceted navigation.

  • hook_post_datafetch.groovy: This runs immediately after the response object is populated based on the raw XML return, but before other response elements are built. This is most commonly used to modify underlying data before the faceted navigation is built.

  • hook_post_process.groovy: This is used to modify the final data model prior to rendering of the search results.

An additional hook script was formerly used to manipulate extra searches and is now deprecated:

  • hook_extra_searches.groovy: (deprecated) This runs after the extra search question is populated but before any extra search runs allowing modification of the extra search’s question. This is deprecated and manipulation of extra searches should be made in other hook scripts (as each extra search runs through the same search lifecycle), but targeting the extra search type. See: writing search lifecycle code.

Creating hook scripts

Hook scripts are a collection configuration file created from the file-manager).

Hook script code takes effect as soon as the hook script is saved.

Writing hook scripts

The hook scripts interact with the search data model.

The variables mirror those used in FreeMarker templates but the question and response response objects are accessed via a parent transaction object. This transaction object is defined for every hook script that runs.

The search question and response is accessed via the following elements of the transaction object:

transaction.question
transaction.response

Hook scripts do not need to implement a specific interface or have a specific header (e.g. imports). You can start writing the body of your script within the first line of the .groovy file. For example the following 1-line hook script appends "cat" to the user submitted query:

transaction.question.query += " cat"

See: wriing search lifecycle code for information on implementing search lifecycle code.

Debugging hook scripts

Compilation problems

If your script doesn’t compile for any reason the error will be logged in the global Modern UI log file: $SEARCH_HOME/web/logs/modernui.(Public/Admin).log.

The most common compilation problems are:

  • Syntax errors.

  • Typos or type errors when accessing the data model, such as using transaction.question.colection instead of transaction.question.collection.

Message logging

If you need to log messages from within your hook script you can do so by using an logger object. A logger object has a name and can log messages with various severity levels (debug, warn, info, error or fatal). To use a logger object from within your script use the following code:

def logger = org.apache.logging.log4j.LogManager.getLogger("com.funnelback.MyHookScript")
...
logger.info("The query is: " + transaction.question.query);
...
logger.fatal("No results were found")

The log messages will be written in one the Modern UI log files:

  • $SEARCH_HOME/web/logs/modernui.[Public/Admin].log.

  • $SEARCH_HOME/data/<collection>/log/modernui.[Public/Admin].log (Linux)

  • $SEARCH_HOME/web/logs/modernui.<collection>.[Public/Admin].log (Windows)

The default configuration of log messages is set to output the error level of above, except for the loggers belonging to the com.funnelback namespace which output info messages. That mean that your messages won’t appear unless:
  • You use logger.error() or logger.fatal()

  • You use logger.info() and your logger belongs to the Funnelback namespace: def logger = org.apache.logging.log4j.LogManager.getLogger("com.funnelback.MyHookScript")

The logging configuration and levels for the Modern UI can be edited in $SEARCH_HOME/web/conf/modernui/log4j.properties.

Examples

Please consult the data model documentation if you’re not familiar with the data model contents (question, response, resultPacket, etc.).

Transforming each result

This example iterates over each result and changes the host name on each live URL using a regular expression. This is a post_datafetch hook because it should happen before the URLs are updated with click tracking information during the output phase.

transaction?.response?.resultPacket?.results.each() {
  // In Groovy, "it" represents the item being iterated
  it.liveUrl = (it.liveUrl =~ /www.badhost.com/).replaceAll("www.correcthost.com")
}

Processing additional input parameters

This example takes a country query string parameter and applies a query constraint on the p metadata class if it exists. Calling http://server.com/s/search?collection=test&query=travel&country=Australia will result in the query travel |p:"Australia" being run.

It’s a pre_process hook because it should be run before the meta_* parameters are transformed to query expressions during the input phase.

def logger = org.apache.logging.log4j.LogManager.getLogger("com.funnelback.hooks.CountryParamHook")
def q = transaction.question
// Set the input parameter 'meta_p_phrase_sand' to the value
// of the 'country' query string parameter, if it exists
if (q.inputParameters.containsKey("country")) {
  q.inputParameters.replaceValues("meta_p_phrase_sand", q.inputParameters.get("country"));
  logger.info("Applied country constraint: " + q.inputParameters.get("country"))
}

Alternatively it could be done as a pre_datafetch hook by providing directly a query expression:

if (q.inputParameters.containsKey("country")) {
  q.metaParameters.add("|p:\"" + q.inputParameters.get("country").get(0) + "\"")
}

Transforming results metadata

This script takes the value of the d and t metadata, concatenates them and put them in the x metadata, for each result. It can be either a post_datafetch or a post_process hook since it doesn’t depend on any transformation done in the output phase.

transaction?.response?.resultPacket?.results.each() {
      // In Groovy, "it" represents the item being iterated
      if (it.listMetadata.containsKey("d") && it.listMetadata.containsKey("t")) {
        it.listMetadata.replaceValues("x", ["Document " + it.listMetadata.get("d").get(0) + " created on: " + it.listMetadata.get("t").get(0)])
      }
   }

Modifying an extra search query

This script modify an extra search to update its query with a metadata constraint. The resulting query expression for this extra search will be <original query> a:shakespeare. This example needs to be in a extra_searches hook since it needs to be run just before the extra search is actually submitted.

if ( transaction.extraSearchesQuestions["myExtraSearch"] != null ) {
  def searchQuestion = transaction.extraSearchesQuestions["myExtraSearch"]
  searchQuestion.query += " a:shakespeare"
}

This hook makes use of the extraSearchesQuestion data model node which contains a SearchQuestion object per configured extra search.

Transforming CGI parameters

Hook scripts can be used to perform CGI parameter transformations. The following example will change the gscope1 parameter depending on the selected form:

if (transaction.question.inputParameters.get("form").stream().findFirst().orElse("") == "scoped") {
  transaction.question.inputParameters.replaceValues("gscope1", ["12"]);
}

This is equivalent to the following CGI Transform:

form=scoped => gscope1=12

Pre-selecting a facet

Pre-selecting a facet is done by injecting the relevant query string parameter in the question. This needs to be done in the pre_process hook script so that the injected parameters are transformed in the relevant query constraints before the query processor is called:

transaction.question.inputParameters.replaceValues("f.Location|X", ["Canberra"])

If you want to pre-select two categories simultaneously for a single facet you must use inputParameters which allow multiple values to be set (See the data model documentation for more details about inputParameters:

transaction.question.inputParameters.replaceValues("f.Location|X", ["Canberra", "Sydney" ])

The syntax of the parameter is f.<FacetName>|<constraint>=<value> where FacetName and constraint are defined in your faceted navigation configuration. The constraint is either a metadata class name such as X, Y, …​ or a gscope number, for example f.Color|1=red.

Query another collection and process the transaction

To perform extra searches using the built in feature is recommended, however you might want to perform an additional query manually for various reasons, such as running an extra query for each result of the main search. To do so you need to fire an HTTP request to the Modern UI and transform the resulting XML into Groovy objects.

import com.funnelback.publicui.xml.SearchXStreamMarshaller
def logger = org.apache.logging.log4j.LogManager.getLogger("com.funnelback.RequestModernUIHookScript")

// Create instance of utility class that will transform our XML back into Java/Groovy objects
def marshaller = new SearchXStreamMarshaller()
// Initialise the marshaller
marshaller.afterPropertiesSet()

// Request the XML for the additional search
// Beware of *not* requesting the same collection or it will get stuck in an infinite loop
def extraTransaction
new URL("http://localhost:8080/s/search.xml?collection=funnelback_documentation&query=best").withInputStream {
  // Unmarshal the XML into a transaction object
  stream -> extraTransaction = marshaller.unmarshalInputStream(stream)
}

// Put the extra transaction in the customData map
transaction.response.customData["mySearch"] = extraTransaction

Read values from a custom configuration file

This example makes use of the Java .properties format. First define your configuration in SEARCH_HOME/conf/<collection>/myConfig.properties:

results.titleSuffix=Suffix
postcode.2000=Sydney
postcode.3000=Melbourne

Then use the following hook script:

def props = new Properties()
new File(transaction.question.collection.configuration.configDirectory, "myConfig.properties").withInputStream {
  stream -> props.load(stream)
}

transaction?.response?.resultPacket?.results.each() {
  // Append suffix to title
  it.title = it.title + props["results.titleSuffix"]
  // Put city name in Z from postcode metadata in X
  it.listMetadata.replaceValues("Z", [props["postcode."+it.listMetadata.get("X").stream().findFirst().orElse("")]])
}