Expressions in Inference Rules - BloomReach Experience - Open Source CMS

This article covers a Hippo CMS version 12. There's an updated version available that covers our most recent release.

30-11-2017

Expressions in Inference Rules

BloomReach offers Enterprise support for this feature to BloomReach Experience customers. The release cycle of this feature may differ from our core product release cycle. 

Introduction

You can define some parameters in the Parameters field for business users to configure something easily and quickly but in the end, the goal value determination from input variables is based on the execution of the expression defined in the Rules Expression field in an Inference Rules document.

As the expression is executed by the internal JEXL engine, the expressions in the Rules Expression field should follow JEXL syntax.

Optionally, you can also configure a Spring bean name or Java class name (FQN) instead of JEXL expression script in the Custom Rules FQN field, in order to execute a Java code implementing the com.onehippo.cms7.inference.engine.api.command.InferenceCommand interface directly. If a non-blank Custom Rules FQN field is set, it will take the precedence over the expression script in Rules Expression field. The Expressional Inference Rule Engine will look up a component from the HST-2 ComponentManager (i.e, HstServices.getComponentManager().getComponent(fqn)) first or it will simply create a new bean by the class name (FQN: fully qualified name) if no component is found from HST-2 ComponentManager.

Goal Value and Classifications

The main reason to evaluate expressions defined in an inference rules document is to determine a goal value (e.g, "news", "events", etc.) from the goal variable (e.g, "content interest category name") if there's any.

In the Relevance Module, to be business-meaningful, data should be classified, whether its variable is categorical, interval, or something else, because that's more intuitive to business uers when applying characteristics in the authoring UI. For example, if the goal variable is a categorical variable (e.g, "content interest category name", "visitor comes from a continent", etc.), the categorical variable can be directly mapped to a goal variable, or the categorical values can be grouped to a goal variable. If the goal variable is an interval variable (e.g, "annual income"), you will probably want to classify it first, like "Low (less than $30K)", "Medium (30-70K)", "High (more than 70K)", for instance. If not classified, it means that business users need to enter an ad hoc expression whenever creating a characteristic. This is problematic for various reasons:

  • It is a lot less intuitive than selecting one or multiple classified values.
  • It is inefficient and ineffective when processing the request log and targeting data stored in the backend later because you have to understand and deal with ad hoc expressions in the JSON properties again.
  • It is more consistent to let business analysts define all the possible goal values of a goal variable in the inference rules document first. Then that makes the goal variable more explicit and maintained in versions to improve in following iterations.

How a Goal Value is Determined?

Expressional Inference Rule Engine executes the expression defined in an inference rule document and checks the return value from the script. If the return value is a non-null value, it converts the value to a String value (i.e, Object#toString()) as a determined goal value. Why String here? It's because a goal value must be a value from a classification (or categorical variable) from a defined goal variable by business analysts in the end. This makes it a lot simpler in both the conceptual perspective and data processing perspective.

Therefore, please keep in mind that the expression script in the inference rule document must return a non-null value if it wants to determine a goal value after the evaluation. If it returns a null value, then it regards the evaluation does not need to determine and store any targeting data, so no targeting data will be deduced (and stored) after evaluation. Here's a simple example script:

// if request URI string contains '/news', then let's determine the goal value this time to be 'news'.
if ($.request.requestURI.contains("/news")) {
    return "news";
}
// or if it contains '/events', then let's determine the goal value this time to be 'events'.
else if ($.request.requestURI.contains("/events")) {
    return "events"; 
}
// otherwise, return null, meaning there's no targeting data this time.
return null;

The script expressions above show simple inference rules. Let R be the request URI string.

  • IF R contains "/news", THEN the goal value is "news".
  • IF R contains "/events", THEN the goal value is "events".
  • OTHERWISE, the goal value is not determined. 

Built-in Objects

Expressional Inference Rule Engine provides the following built-in objects that you can use in the expressions.

 Variable  Description  Type
 $  Built-in root object, containing the parameters, attribute and other objects.

com.onehippo.cms7.inference.engine.
api.model.GenericBuiltinModel

 $.logger  Logger object with which expressions can leave logs.

com.onehippo.cms7.inference.engine.
api.model.GenericLoggerModel

 $.request  Request object with properties and methods about HttpServletRequest and HstRequestContext.

com.onehippo.cms7.inference.engine.
api.model.GenericRequestContextModel

 $.time  Date and time object to provide current datetime.

com.onehippo.cms7.inference.engine.
api.model.GenericTimeModel

 $.collectorContext  Targeting data collector context object to provide context attributes in Targeting Collector execution.

com.onehippo.cms7.inference.engine.
api.model.GenericCollectorContextModel

Each object has the following properties and operations.

$ (The Built-in root object)

/**
 * Built-in root object in inference rules expressions.
 */
public interface GenericBuiltinModel extends GenericModel {

    /**
     * Return the set of defined parameter names.
     * @return the set of defined parameter names.
     */
    public Set<String> getParameterNames();

    /**
     * Return the first parameter value by the parameter name. Null if there's no parameter by the name.
     * If there are multiple parameter values, the the first parameter is returned.
     * @param name parameter name
     * @return the first parameter value by the parameter name. Null if there's no parameter by the name.
     */
    public String getParameter(String name);

    /**
     * Return parameter values array by the parameter name. Null if parameter name is not found.
     * @param name parameter name
     * @return parameter values array by the parameter name. Null if parameter name is not found.
     */
    public String[] getParameterValues(String name);

    /**
     * Return logger that can be used by expressions to leave logs.
     * @return logger logger that can be used by expressions to leave logs
     */
    public GenericLoggerModel getLogger();

    /**
     * Request representation object to retrieve <code>HttpServletRequest</code> or <code>HstRequestContext</code>
     * specific data.
     * @return representation object to retrieve <code>HttpServletRequest</code> or <code>HstRequestContext</code>
     * specific data.
     */
    public GenericRequestContextModel getRequest();

    /**
     * Date/time representation object to retrieve the current datetime information.
     * @return Date/time representation object to retrieve the current datetime information
     */
    public GenericTimeModel getTime();

    /**
     * Return true if the expression is being executed in the targeting collector execution context.
     * @return true if the expression is being executed in the targeting collector execution context
     */
    public boolean hasCollectorContext();

    /**
     * Return the targeting collector context object. Null if the expression is not being executed in the targeting
     * collector execution context.
     * @return the targeting collector context object. Null if the expression is not being executed in the targeting
     * collector execution context
     */
    public GenericCollectorContextModel getCollectorContext();

    /**
     * Return an iterable over all the attribute names.
     * @return an iterable over all the attribute names
     */
    public Iterable<String> getAttributeNames();

    /**
     * Return true if there exists an attribute by the name.
     * @param name attribute name
     * @return true if there exists an attribute by the name
     */
    public boolean hasAttribute(String name);

    /**
     * Return the attribute object by the name.
     * @param name the attribute name
     * @return the attribute object by the name
     */
    public Object getAttribute(String name);

    /**
     * Set an attribute object by the name.
     * @param name attribute name
     * @param value attribute object value
     */
    public void setAttribute(String name, Object value);

    /**
     * Remove attribute object having the name.
     * @param name attribute name
     */
    public void removeAttr(String name);

}

$.logger

/**
 * Logger object in inference rules expressions, wrapping the SLF4J Logger interface.
 */
public interface GenericLoggerModel extends GenericModel {

    public String getName();

    public boolean isTraceEnabled();

    public void trace(String msg);

    public void trace(String format, Object arg);

    public void trace(String format, Object arg1, Object arg2);

    public void trace(String format, Object... arguments);

    public void trace(String msg, Throwable t);

    public boolean isDebugEnabled();

    public void debug(String msg);

    public void debug(String format, Object arg);

    public void debug(String format, Object arg1, Object arg2);

    public void debug(String format, Object... arguments);

    public void debug(String msg, Throwable t);

    public boolean isInfoEnabled();

    public void info(String msg);

    public void info(String format, Object arg);

    public void info(String format, Object arg1, Object arg2);

    public void info(String format, Object... arguments);

    public void info(String msg, Throwable t);

    public boolean isWarnEnabled();

    public void warn(String msg);

    public void warn(String format, Object arg);

    public void warn(String format, Object... arguments);

    public void warn(String format, Object arg1, Object arg2);

    public void warn(String msg, Throwable t);

    public boolean isErrorEnabled();

    public void error(String msg);

    public void error(String format, Object arg);

    public void error(String format, Object arg1, Object arg2);

    public void error(String format, Object... arguments);

    public void error(String msg, Throwable t);

}

$.request

/**
 * ServletRequest representation object that wraps HttpServletRequest and HstReuqestContext instances.
 */
public interface GenericServletRequestModel extends GenericModel {

    public String getParameter(String name);

    public List<String> getParameterNames();

    public List<String> getParameterValues(String name);

    public String getScheme();

    public String getServerName();

    public int getServerPort();

    public String getRemoteAddr();

    public Locale getLocale();

    public String getAuthType();

    public Map<String, Cookie> getCookies();

    public String getHeader(String name);

    public long getDateHeader(String name);

    public int getIntHeader(String name);

    public List<String> getHeaders(String name);

    public List<String> getHeaderNames();

    public String getMethod();

    public String getPathInfo();

    public String getPathTranslated();

    public String getContextPath();

    public String getQueryString();

    public String getRemoteUser();

    public java.security.Principal getUserPrincipal();

    public String getRequestURI();

    public String getRequestURL();
}

/**
 * HstRequestContext representation object in inference rules expressions,
 * extending GenericServletRequestModel.
 */
public interface GenericRequestContextModel extends GenericServletRequestModel {

    /**
     * Return content bean model object representing HST Content Bean returned from
     * HstRequestContext#getContentBean().
     * @return
     */
    public GenericContentBeanModel getContent();

}

$.time

/**
 * Date/time representation object in inference rules expressions, to return the current datetime information.
 */
public interface GenericTimeModel extends GenericModel {

    public Date getTime();

    public long getTimeInMillis();

    public TimeZone getTimeZone();

    public int getYear();

    public int getMonth();

    public int getDate();

    public int getWeekOfYear();

    public int getWeekOfMonth();

    public int getDayOfMonth();

    public int getDayOfWeek();

    public int getDayOfWeekInMonth();

    public int getDayOfYear();

    public int getHourOfDay();

    public boolean isPm();

    public int getHour();

    public int getMinute();

    public int getSecond();

    public int getMillisecond();

}

$.collectorContext

/**
 * Targeting Collector execution context representation object.
 */
public interface GenericCollectorContextModel extends GenericModel {

    /**
     * Return true if the targeting collector is being executed for a new visitor.
     * @return true if the targeting collector is being executed for a new visitor
     */
    public boolean isNewVisitor();

    /**
     * Return true if the targeting collector is being executed for a new visit of a visitor.
     * @return true if the targeting collector is being executed for a new visit of a visitor
     */
    public boolean isNewVisit();

    /**
     * Return extra targeting data map.
     * @return extra targeting data map
     */
    public Map<String, Object> getExtraData();

    /**
     * Return request level goal value which could be different from the targeting goal value.
     * @return request level goal value
     */
    public Object getRequestLevelGoalValue();

    /**
     * Set request level goal value which could be different from the targeting goal value.
     * @param requestLevelGoalValue request level goal value
     */
    public void setRequestLevelGoalValue(Object requestLevelGoalValue);

    /**
     * Return the custom persona evaluation score determined by the expression. -1.0 if not determined by expression.
     * Only meaningful when this returns a number in the range of [0.0, 1.0].
     * @return the custom persona evaluation score determined by the expression. -1.0 if not determined by expression
     */
    public double getPersonaEvaluationScore();

    /**
     * Sets the custom persona evaluation score determined by the expression.
     * @param personaEvaluationScore the custom persona evaluation score determined by the expression
     */
    public void setPersonaEvaluationScore(double personaEvaluationScore);

}

Built-in Function Namespaces

Expressional Inference Rule Engine also provides the following built-in function namespaces that you can use in the expressions.

 Function Namespace  Description  Type
 string:  String utilities. e.g, string:split("Hello, World!", ",");

org.apache.commons.lang.StringUtils

 arrays:  Array access utilities. e.g, arrays:length(arr); arrays:get(arr, 0);

com.onehippo.cms7.inference.engine.core.util.ArraysUtils

 array:  Array utilities provided by Commons Lang.

org.apache.commons.lang.ArrayUtils

 locale:  Locale utilities provided by Commons Lang.

org.apache.commons.lang.LocaleUtils

 date:  Date utilities provided by Commons Lang.

org.apache.commons.lang.time.DateUtils

 dateformat:  Date formatting utilities provided by Commons Lang. 

org.apache.commons.lang.time.DateFormatUtils

 durationformat:  Time duration utilities provided by Commons Lang.

org.apache.commons.lang.time.DurationFormatUtils

 number:  Number utilities provided by Commons Lang.

org.apache.commons.lang.math.RandomUtils

 random:  Random number utilities provided by Commons Lang.

org.apache.commons.lang.math.RandomUtils

 collection:  java.util.Collection utilities provided by Commons Lang.

org.apache.commons.collections.CollectionUtils

 enumaration:  java.util.Enumartion utilities provided by Commons Lang.

org.apache.commons.collections.EnumerationUtils

 iterator:  java.util.Iterator utilities provided by Commons Lang.

org.apache.commons.collections.IteratorUtils

 list:  java.util.List utilities provided by Commons Lang.

org.apache.commons.collections.ListUtils

 map:  java.util.Map utilities provided by Commons Lang.

org.apache.commons.collections.MapUtils

  set:  java.util.Set utilities provided by Commons Lang.

org.apache.commons.collections.SetUtils

 counter:  Counting utilities. For example, it provides utilities to increment counter by key in a map. e.g, var map = counter:newMap(); counter:increment(map, "key1");

com.onehippo.cms7.inference.engine.api.util.CounterUtils

 resourcebundle:  java.util.ResourceBundle utilities provided by Commons Lang, also supporting HST-2 Dynamic Resource Bundles.

com.onehippo.cms7.inference.engine.core.util.ResourceBundleUtils

 regex:  Regular expression utilities that provide compiling both regex expressions and glob expressions.

com.onehippo.cms7.inference.engine.core.util.RegexUtils

 json:  JSON utilities providing parsed net.sf.json.JSONObject or net.sf.json.JSONArray objects.

com.onehippo.cms7.inference.engine.core.util.JsonUtils

 yaml:  YAML utilities providing parsed org.yaml.snakeyaml.nodes.Node objects.

com.onehippo.cms7.inference.engine.core.util.YamlUtils

 geolocation:

 GEO Location utilities to find location by client IP address.

com.onehippo.cms7.inference.engine.core.util.GenericGeoLocationUtils

.

How to Store Extra Targeting Data?

When implementing inference rules expressions in practice, sometimes you need to store some extra data in addition to the goal value. For example, a business analyst wants to collect a goal variable, "the most frequent content interest category", from visitors. In this case, the goal value cannot be determined each time on each request. The data collection should store a counter map for each category and determine the most frequent category name instead. Here's an example sequence of requests, showing how an extra counter map can be stored to determine the designed goal value:

 Request Sequence  Interest Category of this Request  Interest Category Counter Map

 Goal value determined

 1  "news"  { "news": 1 }  "news"
 2  "events"  { "news": 1, "events": 1 }  "news" or "events"
 3  "events"  { "news": 1, "events": 2 }  "events"
 4  "unknown"  { "news": 1, "events": 2, "unknown": 1 }  "events"

In this example scenario, if you don't keep an extra counter map data in the targeting data store, you won't be able to deduce the designed goal value.

In order to store an extra data (like the counter map), you can put the extra data (like the counter map) in $.collectorContext.extraData (type of java.util.Map). If any data is stored into $.collectorContext.extraData, then that extra data will be saved to the targeting data store and read it again from the targeting data store automatically.

Example Script

Here's a full example expression script to support an example goal variable, "the most frequent content interest category", from visitors. You can find this example in the demo project, too:

// The primary goal data to infer from various inputs.
var interestType = "unknown";

// Just as an example, input variables here are the current request URI and/or the 'Referer' http header.
var requestURI = $.request.requestURI;
var referer = $.request.getHeader("Referer");

// If it contains either '/events' or '/news', the goal value is determined accordingly.
for (var paramName : $.parameterNames) {
    if (paramName.startsWith("goal.uri.mapping.")) {
        var paramValue = $.getParameter(paramName);
        var pair = string:split(paramValue, " :"); var type = arrays:get(pair, 0); var uri = arrays:get(pair, 1);
        if (requestURI.contains(uri) or referer.contains(uri)) {
            interestType = type;
            break;
        }
    }
}

// If the interestType goal value was determined,
// and if this is invoked in the relevance collector context,
// you can store extra goal data values map ($.collectorContext.extraData)
// and increment the counter for the goal value to store it back to the targeting data.
if ($.hasCollectorContext()) {
    // Sets the request level goal value before determining the max counter valued type by counterMap.
    $.collectorContext.setRequestLevelGoalValue(interestType);
    $.logger.debug("requestLevelGoalValue: {}", interestType);
    // Get the counter map for each interest type goal value.
    var counterMap = $.collectorContext.extraData.get("counterMap");
    // If the counter map doesn't exist yet, create a new one and put it back to extra data map.
    if (counterMap == null) {
        counterMap = counter:newMap();
        $.collectorContext.extraData.put("counterMap", counterMap);
    }
    interestType = counter:incrementAndGetMaxKey(counterMap, interestType);
    $.logger.debug("counterMap: {}", counterMap);
}

// For an easier extension/integration, let's see how you can add/read extra attributes.
var fooConnector = $.getAttribute("fooMarketingConnector");
var account = fooConnector != null ? fooConnector.getAccount() : null;
if (account != null) {
    $.logger.debug("Account : " + account);
}

// Leave a log and return the primary goal value, interestType, finally.
$.logger.debug("interestType return: {}", interestType);
return interestType;

(Optional) Using InferenceCommand Bean instead of JEXL Expressions

Optionally, you can also configure a Spring bean name or Java class name (FQN) instead of JEXL expression script in the Custom Rules FQN field, in order to execute a Java code implementing com.onehippo.cms7.inference.engine.api.command.InferenceCommand interface directly. If a non-blank Custom Rules FQN field is set, it will take the precedence over the expression script in Rules Expression field. The Expressional Inference Rule Engine will look up a component from HST-2 ComponentManager (i.e, HstServices.getComponentManager().getComponent(fqn)) first or it will simply create a new bean by the class name (FQN: fully qualified name) if no component is found from HST-2 ComponentManager.

You can implement com.onehippo.cms7.inference.engine.api.command.InferenceCommand interface in your command class from scratch, but it will be much easier to let your command class extend com.onehippo.cms7.inference.engine.api.command.AbstractInferenceCommand base class since it provides utility methods to retrieve the root built-in objects. For example, AbstractInferenceCommand#getBuiltin() returns the GenericBuiltinModel instance (referenced as $ variable in JEXL expressions as explained in the previous sections).

Here's an example:

/*
 *  Copyright 2017 Hippo B.V. (http://www.onehippo.com)
 */
package com.onehippo.cms7.inference.engine.demo.integration;

import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.onehippo.cms7.inference.engine.api.command.AbstractInferenceCommand;
import com.onehippo.cms7.inference.engine.api.command.InferenceCommandContext;
import com.onehippo.cms7.inference.engine.api.model.GenericCollectorContextModel;
import com.onehippo.cms7.inference.engine.api.model.GenericRequestContextModel;
import com.onehippo.cms7.inference.engine.api.util.CounterUtils;

public class DemoContentInterestTypeInferenceCommand extends AbstractInferenceCommand {

    private static Logger log = LoggerFactory.getLogger(DemoContentInterestTypeInferenceCommand.class);

    @Override
    protected Object executeInternal(InferenceCommandContext commandContext) {
        // The primary goal data to infer from various inputs.
        String interestType = "unknown";

        final GenericRequestContextModel request = getBuiltin().getRequest();

        // Just as an example, input variables here are the current request URI and/or the 'Referer' http header.
        final String requestURI = request.getRequestURI();
        final String referer = request.getHeader("Referer");

        // If it contains either '/events' or '/news', the goal value is determined accordingly.
        for (String paramName : getBuiltin().getParameterNames()) {
            if (paramName.startsWith("goal.uri.mapping.")) {
                String paramValue = getBuiltin().getParameter(paramName);
                String [] pair = StringUtils.split(paramValue, " :");
                String type = pair[0];
                String uri = pair[1];

                if (requestURI.contains(uri) || referer.contains(uri)) {
                    interestType = type;
                    break;
                }
            }
        }

        // If the interestType goal value was determined,
        // and if this is invoked in the relevance collector context,
        // you can store extra goal data values map ($.collectorContext.extraData)
        // and increment the counter for the goal value to store it back to the targeting data.
        if (getBuiltin().hasCollectorContext()) {
            // Get the counter map for each interest type goal value.
            final GenericCollectorContextModel collectorContext = getBuiltin().getCollectorContext();

            // Sets the request level goal value before determining the max counter valued type by counterMap.
            collectorContext.setRequestLevelGoalValue(interestType);
            log.debug("requestLevelGoalValue: {}", interestType);

            Map<Object, Number> counterMap = (Map<Object, Number>) collectorContext.getExtraData().get("counterMap");
            // If the counter map doesn't exist yet, create a new one and put it back to extra data map.
            if (counterMap == null) {
                counterMap = CounterUtils.newMap();
                getBuiltin().getCollectorContext().getExtraData().put("counterMap", counterMap);
            }
            interestType = (String) CounterUtils.incrementAndGetMaxKey(counterMap, interestType);
            log.debug("counterMap: {}", counterMap);

            // Just for demonstration purpose, let's show how you can set a custom persona evaluation score like this.
            // Otherwise, by default, the default scorer will set 1.0 for matched classifications.
            collectorContext.setPersonaEvaluationScore(0.9);
        }

        // For an easier extension/integration, let's see how you can add/read extra attributes.
        FooMarketingConnector fooConnector = (FooMarketingConnector) getBuiltin().getAttribute("fooMarketingConnector");
        FooMarketingAccount account = fooConnector != null ? fooConnector.getAccount() : null;
        if (account != null) {
            log.debug("Account : {}", account);
        }

        // Leave a log and return the primary goal value, interestType, finally.
        log.debug("interestType return: {}", interestType);
        return interestType;
    }

}

If you set the Custom Rules FQN field to the FQCN (com.onehippo.cms7.inference.engine.demo.integration.DemoContentInterestTypeInferenceCommand in this example) for instance, then Expressional Inference Rule Engine will simply create a new instance based on the FQCN of the command class by default.

But if you define a Spring Bean in an bean assembly XML file (e.g, inference-commands.xml) under site/src/main/resources/META-INF/hst-assembly/overrides/ like the following example and if you set the Custom Rules FQN field to the example bean ID ("demoContentInterestTypeInferenceCommand" in this example) for instance, then Expressional Inference Rule Engine will look up the bean by the name from HST-2 ComponentManager instead.

<?xml version="1.0" encoding="UTF-8"?>
<!--
  Copyright 2017 Hippo B.V. (http://www.onehippo.com)
  -->

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="demoContentInterestTypeInferenceCommand"
        class="com.onehippo.cms7.inference.engine.demo.integration.DemoContentInterestTypeInferenceCommand">
  </bean>

</beans>

In priciple, it would be more efficient to take the Spring bean approach instead of creating an instance by the class name (FQCN) and let the Spring bean behave in a thread-safe manner.

Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?