Faceted navigation customization

The plugin framework allows faceted navigation to be customized in a couple of ways:

Faceted navigation configuration

Include faceted navigation configuration to support a plugin by implementing the FacetProvider interface. For example, if you were writing an Instagram custom gather plugin you may wish to provide some Instagram-specific facets as part of the plugin, which are added when the plugin is enabled.

Faceted navigation custom sort

Implement a custom sorting algorithm that can be used to sort your faceted navigation categories by implementing a SearchLifeCycle plugin that calls a custom comparator.

Provide faceted navigation configuration for a plugin

Interface methods

To provide faceted navigation configuration for a plugin, you need to implement the FacetProvider interface.

The FacetProvider interface has a single method:

String extraFacetedNavigation(IndexConfigProviderContext context)

Since facets are defined at a results page level, the IndexConfigProviderContext provides both a collection (parent search package ID) and profile (results page ID), and extraFacetedNavigation will be run for every results page.

Additional facets can be supplied by returning a JSON similar to the API.

GET /faceted-navigation/v2/collections/{collection}/profiles/{profile}/facet/{id}/views/{view}

This expects to return a list [] of facets. The id, lastModified and created fields do not need to be set.

When building a plugin, configure the faceted navigation using the faceted navigation configuration screen in the search dashboard. After configuring your facets, copy the JSON from the facet configuration JSON tab on the preview screen for the facet and omit the id, lastModified and created fields.

For example:

[{
  "name": "Instagram Authors",
  "facetValues": "FROM_SCOPED_QUERY_HIDE_UNSELECTED_PARENT_VALUES",
  "constraintJoin": "AND",
  "selectionType": "SINGLE",
  "categories": [{
    "type": "MetaDataFieldCategory",
    "subCategories": [],
    "metadataField": "instagramAuthor"
  }],
  "order": [ "SELECTED_FIRST", "COUNT_DESCENDING"]
}]
Facet names must be unique within the results page. Facets defined for a results page will be used in preference to facets defined from the plugin if both define a facet with the same name.

Usage

Facets will automatically start appearing in search results after the plugin is enabled on the results page.

Some facets will require a reindex to take effect.

Logging

Log messages from the extraFacetedNavigation method will appear in the search package’s user interface logs.

Implement a custom sort algorithm for faceted navigation

The sorting of faceted navigation categories is set as part of the facet configuration. The sort option provides a number of sort criteria as well as a custom sort option.

When custom is selected it requires a custom sort comparator to be enabled, and this is implemented in a plugin.

Faceted navigation custom sort is implemented in a search lifecycle plugin that implements a post process re-sorting of the faceted navigation categories within the data model.

Create a faceted navigation custom sort plugin

To create a faceted navigation custom sort plugin:

  1. Create a new plugin that implements the search lifecycle plugin interface. This must implement code that reorders the facet categories of the facet that has custom sort enabled.

  2. Create a class that implements the custom sort logic using a Java comparator. Call this comparator from the post-process reorder method.

Using a faceted navigation custom sort plugin

  1. Enable the plugin that implements the custom sorting

  2. Edit your faceted navigation configuration and set the category sorting to custom for the facets that you wish to sort with the plugin.

Funnelback currently only supports the use of a single custom sorting plugin per results page.

Example: Faceted navigation custom sort comparator

The code below implements a post-process hook that calls a method to reorder the facet categories of the targeted facet.

FacetsCustomSortPositionSearchLifeCyclePlugin.java
package com.funnelback.plugin.facetsCustomSortPosition;

import com.funnelback.publicui.search.model.collection.ServiceConfig;
import com.funnelback.publicui.search.model.transaction.Facet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.funnelback.plugin.SearchLifeCyclePlugin;
import com.funnelback.publicui.search.model.transaction.SearchTransaction;

import java.util.List;

import static com.funnelback.plugin.facetsCustomSortPosition.PluginUtils.*;

public class FacetsCustomSortPositionSearchLifeCyclePlugin implements SearchLifeCyclePlugin {

    private static final Logger log = LogManager.getLogger(FacetsCustomSortPositionSearchLifeCyclePlugin.class);

    @Override
    public void postProcess(SearchTransaction transaction) {
        List<Facet> facets = transaction.getResponse().getFacets();
        if (facets.isEmpty()) {
            log.info("No facet navigation is configured so the plugin will not run.");
            return;
        }

        if (transaction.getQuestion().getCurrentProfileConfig().getConfigKeysWithPrefix(KEY_PREFIX).isEmpty()) {
            log.info("No configuration for plugin is provided so the plugin will not run.");
            return;
        }

        reorderFacets(facets, transaction.getQuestion().getCurrentProfileConfig());
    }

    protected void reorderFacets(List<Facet> facets, ServiceConfig config) {
        facets.forEach(facet -> {
            if (config.getConfigKeysWithPrefix(KEY_PREFIX + facet.getName()).isEmpty()) {
                return;
            }

            final List<String> first = getConfigKeyValue(config, getConfigKey(facet, KEY_SUFFIX_SORT_ORDER_FIRST));
            final List<String> last = getConfigKeyValue(config, getConfigKey(facet, KEY_SUFFIX_SORT_ORDER_LAST));
            if (first.isEmpty() && last.isEmpty()) {
                return;
            }

            log.debug("Run plugin by enabling custom sorting for '{}' facet", facet.getName());
            facet.setCustomComparator(new FacetsCustomSortPositionComparator(first, last));
        });
    }
}

The class below implements the custom sorting logic within a Java comparator. This comparator promotes or demotes specific category values resulting in a custom sort order for the faceted navigation categories.

FacetsCustomSortPositionComparator.java
package com.funnelback.plugin.facetsCustomSortPosition;

import com.funnelback.publicui.search.model.transaction.Facet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

public class FacetsCustomSortPositionComparator implements Comparator<Facet.CategoryValue> {
    private static final Logger log = LogManager.getLogger(FacetsCustomSortPositionComparator.class);
    private HashMap<String, String> order = new HashMap<String, String>();

    /**
     *
     * @param firstPosition list of category labels to be ordered at the beginning
     * @param lastPosition list of category labels to be ordered at the end
     */
    FacetsCustomSortPositionComparator(List<String> firstPosition, List<String> lastPosition) {
        final String o = "\u0000\u0000\u0000"; // smallest code point, equivalent of NUL char
        final String f = "\uFFFE\uFFFE\uFFFE"; // one of the last code points, non-character
        setOrder(firstPosition, o);
        setOrder(lastPosition, f);
        log.debug("Facet categories will by sorted by provided custom order {}", order);
    }

    @Override
    public int compare(Facet.CategoryValue cv1, Facet.CategoryValue cv2) {
        String l1 = getOrderKey(cv1.getLabel());
        String l2 = getOrderKey(cv2.getLabel());
        Boolean toCompare = false;

        if (order.containsKey(l1)) {
            l1 = order.get(l1);
            toCompare = true;
        }
        if (order.containsKey(l2)) {
            l2 = order.get(l2);
            toCompare = true;
        }
        return toCompare ? l1.compareTo(l2) : 0;
    }

    /**
     * Get lower-cased category label
     * @param label
     */
    private String getOrderKey(String label) {
        return label.toLowerCase(Locale.ROOT);
    }

    /**
     * Create order key use to sort category labels
     * @param orderPrefix string prefix used to ensure value will be marked as one of the first or last
     * @param orderPosition for multiple category labels maintain the order from configuration
     */
    private String getOrderVal(String orderPrefix, int orderPosition) {
        return orderPrefix + String.format("%06d", orderPosition);
    }

    /**
     * Create order list of category labels
     * @param categories list of category labels to be ordered
     * @param orderPrefix string prefix used to ensure value will be marked as one of the first or last
     */
    private void setOrder(List<String>categories, String orderPrefix) {
        for (int i = 0; i < categories.size(); i++) {
            order.put(getOrderKey(categories.get(i)), getOrderVal(orderPrefix, i));
        }
    }
}