Creating a Custom Facet in Coveo Hive – Part Two

In my previous blog post, I went over the basics of what I needed to do to set up a custom facet / component in Coveo Hive. In this blog post, I will take a deeper dive into how we can have our custom facet interact with the current Coveo query on the search page and manage Coveo state. Let’s get started.

Updating the Current Coveo Query

At this point, you should have a visible and interact-able facet. When a user enters a value into a field of your facet, checks a box, or makes some other type of facet interaction, you’ll want to make sure the Coveo search page reflects this interaction by showing updated search results. In technical terms, we can achieve this by adding our custom facet interactions to the advanced expression of the current query. We will need to do the following:

  1. Add code to process the query change.
    1. Update the Coveo state.
    2. Then, call executeQuery() to run the buildingQuery event, which will set the advanced expression and cause the search results component to refresh.
    3. Finally, log the search event to Coveo Usage Analytics (optional).

Processing the Query Change

You’ll want to add a new function in the script file for your custom component. As I stated in the previous blog post, if you opted to use Typescript instead of Javascript, your code will be slightly different. I’m assuming you already added the necessary event handlers and validation for the inputs within your custom facet, but if not, you’ll want to do that now. Then, if the data entered into your facet is valid, call your new function to process the query change. We’ll call it processQueryChange.

Update Coveo State

The first thing we’ll be doing is adding our custom facet interaction to the Coveo state. Before we do that though, we need to do a couple things.

We need to register a new state attribute by hooking into the afterComponentsInitialization Coveo event. In my case, my field was named aoparesultdate and represented a date range. When you register the state attribute, you also have to set the default value, and in my case, it was an empty array, so my code looked like this (you may remember from the previous blog post that coveoClass is a reference to my Javascript class):

 AOPADateRangePickerFacet.prototype.registerStateAttribute = function (coveoClass) {
     Coveo.$(this.bindings.searchInterface.element).on("afterComponentsInitialization", function (e) {
         coveoClass.bindings.searchInterface.element.Coveostate.registerNewAttribute("f:@aoparesultdate:range", []);
     });
 };

Second, you may need to sanitize the inputs further. For example, if you are in my situation where your custom facet will have date fields, you actually have to convert those date values to Unix timestamps. Why? Well, when you want to add a date range to Coveo state, it must be in the following format:

[XX.XXXXX (start date as Unix timestamp), YY.YYYYY (end date as Unix timestamp)]

So you’ll have to convert the dates and “concatenate” them into a two decimal array of Unix timestamps:

  var startDateUnix = (StartDate.length === 0 ? 0 : (new Date(StartDate).getTime()));
 var endDateUnix = (EndDate.length === 0 ? 0 : (new Date(EndDate).getTime()));
 var newArray = (startDateUnix === 0 && endDateUnix === 0 ? [] : [startDateUnix, endDateUnix]);

Anyway, if you don’t have dates or a date range, all you have to do is add to the Coveo state like this:

  if (searchInterface.Coveostate !== undefined) {
     searchInterface.Coveostate.set('f:@yourFacetName', yourNewFacetValue);
 }

Several things to note here:

  • This adds a query string to the URL in your browser like this: &f:@yourFacetName=[Value].
  • If your new facet value represents a range of Unix timestamps as in my case, you’ll need to add :range after f:@yourFacetName.
  • If you’re not sure what type of value to give to the Coveo state, it might help you to check what is in there currently. You can do this by entering the following into the console:

$('.CoveoSearchInterface')[0].Coveostate

Here, I am getting the search interface div and checking the Coveostate property. Your search interface might not have the same class, so you might have to update the jQuery selector to get your search interface. Now, to see what is currently in the Coveostate, expand t then attributes and you will see all the current state attributes. They also hint at what type of value they accept – object, string, array, etc.

coveostate in console

Execute the Query

Next, you will want to call executeQuery() on the query controller like so:

 coveoClass.bindings.queryController.executeQuery();

This command will run the Coveo buildingQuery event, and you’ll need to handle that event so that you can set the advanced expression.

Remember: Whenever you interact with a component of your Coveo search page, the buildingQuery event is initiated. This is a Coveo event that you can handle and run custom code when it occurs.

First, you’ll need to register that event so it can be handled:

  AOPADateRangePickerFacet.prototype.registerBuildingQueryEvent = function (coveoClass) {
     Coveo.$(this.bindings.searchInterface.element).on('buildingQuery', function (e, args) {
         coveoClass.handleBuildingQuery(e, args, coveoClass);
     });
 };

Then, create a new function named handleBuildingQuery where you will check your Coveo state attribute and use it to set the advanced expression. The following is code specified to my requirements, but it should give you an idea on how this is done.

 AOPADateRangePickerFacet.prototype.handleBuildingQuery = function (e, args, coveoClass) {
    var dateField = $(coveoClass.startDateField).closest('div.CoveoAOPADateRangePickerFacet').attr('data-field');
    var newDateSet = coveoClass.getNewDateSet(coveoClass);
    if (newDateSet) {
        NewDateSet = newDateSet;
        StartDate = $(coveoClass.startDateField).val();
        EndDate = $(coveoClass.endDateField).val();

       // This becomes '@aoparesultdate>=2018/1/1' for example
       // check state for our custom field
       var resultDateValue = coveoClass.bindings.searchInterface.element.Coveostate.attributes["f:@aoparesultdate:range"];
       if (resultDateValue !== undefined && resultDateValue.length > 0) {
           var expressionString = dateField + newDateSet;
           args.queryBuilder.advancedExpression.add(expressionString);
       }
    }
 };

The logic is, we want to make sure there is a value for our custom facet in Coveo state before updating the query. So once that is done, we create an expression string, which in my case looks like @aoparesultdate>=2018/1/1. It should contain the field name, an operator and the value to use. Then, just add it to the advancedExpression and you will see the search results change!

Logging the Event to Coveo Usage Analytics (Optional)

If you’d like, you can easily log analytics events after calling executeQuery(), like so:

  this.bindings.usageAnalytics.logSearchEvent({
     name: 'facetSelect',
     type: 'facet'
 });

Final Code for this Piece

...
    AOPADateRangePickerFacet.prototype.registerStateAttribute = function (coveoClass) {
        Coveo.$(this.bindings.searchInterface.element).on("afterComponentsInitialization", function (e) {
            coveoClass.bindings.searchInterface.element.Coveostate.registerNewAttribute("f:@aoparesultdate:range", []);
        });
    };

    AOPADateRangePickerFacet.prototype.registerBuildingQueryEvent = function (coveoClass) {
        Coveo.$(this.bindings.searchInterface.element).on('buildingQuery', function (e, args) {
            coveoClass.handleBuildingQuery(e, args, coveoClass);
        });
    };

AOPADateRangePickerFacet.prototype.processQueryChange = function (coveoClass) {
        // Update state
        var startDateUnix = (StartDate.length === 0 ? 0 : (new Date(StartDate).getTime()));
        var endDateUnix = (EndDate.length === 0 ? 0 : (new Date(EndDate).getTime()));
        var searchInterface = coveoClass.bindings.searchInterface.element;
        var newArray = (startDateUnix === 0 && endDateUnix === 0 ? [] : [startDateUnix, endDateUnix]);
        if (searchInterface.Coveostate !== undefined) {
            searchInterface.Coveostate.set('f:@aoparesultdate:range', newArray);
        }

        coveoClass.bindings.queryController.executeQuery();

        this.bindings.usageAnalytics.logSearchEvent({
            name: 'facetSelect',
            type: 'facet'
        });
    };

    AOPADateRangePickerFacet.prototype.handleBuildingQuery = function (e, args, coveoClass) {
        var dateField = $(coveoClass.startDateField).closest('div.CoveoAOPADateRangePickerFacet').attr('data-field');
        var newDateSet = coveoClass.getNewDateSet(coveoClass);
        if (newDateSet) {
            NewDateSet = newDateSet;
            StartDate = $(coveoClass.startDateField).val();
            EndDate = $(coveoClass.endDateField).val();

            // This becomes '@aoparesultdate>=2018/1/1' for example
            // check state for our custom field
            var resultDateValue = coveoClass.bindings.searchInterface.element.Coveostate.attributes["f:@aoparesultdate:range"];
            if (resultDateValue !== undefined && resultDateValue.length > 0) {
                var expressionString = dateField + newDateSet;
                args.queryBuilder.advancedExpression.add(expressionString);
            }
        }
    };

AOPADateRangePickerFacet.prototype.getNewDateSet = function (coveoClass) {
        var startDateVal = $(coveoClass.startDateField).val();
        var endDateVal = $(coveoClass.endDateField).val();
        var newDateSet = '';
        var startDateYear = '';
        var startDateMonth = '';
        var startDateDay = '';
        var endDateYear = '';
        var endDateMonth = '';
        var endDateDay = '';
        var startDate = '';
        var endDate = '';

        // Reformat dates. Since Coveo JS framework seems to only accept in format yyyy/mm/dd?
        if (startDateVal.length) {
            startDate = new Date(startDateVal);
            startDateYear = startDate.getFullYear();
            startDateMonth = startDate.getMonth() + 1;
            startDateDay = startDate.getDate();

            if (startDateMonth.toString().length < 2) startDateMonth = '0' + startDateMonth;
            if (startDateDay.toString().length < 2) startDateDay = '0' + startDateDay;
        };

        if (endDateVal.length) {
            endDate = new Date(endDateVal);
            endDateYear = endDate.getFullYear();
            endDateMonth = endDate.getMonth() + 1;
            endDateDay = endDate.getDate();

            if (endDateMonth.toString().length < 2) endDateMonth = '0' + endDateMonth;
            if (endDateDay.toString().length =' + reformattedStartDate;
        } else if (!startDateVal.length && endDateVal.length) {
            newDateSet = '<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;">&#65279;</span>&lt;=&#039; + reformattedEndDate;
        } else return;

        return newDateSet;
    };
...

Status Check

You’re at the point now where interacting with your custom facet will update the search results in your search page. Additionally, the query string should change as you update the value in your custom facet.

Next Steps

In the next and final blog post, we will go over how to implement and manage breadcrumbs for your custom facet. Stay tuned!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s