search mobile facets autocomplete spellcheck crawler rankings weights synonyms analytics engage api customize documentation install setup technology content domains user history info home business cart chart contact email activate analyticsalt analytics autocomplete cart contact content crawling custom documentation domains email engage faceted history info install mobile person querybuilder search setup spellcheck synonyms weights engage_search_term engage_related_content engage_next_results engage_personalized_results engage_recent_results success add arrow-down arrow-left arrow-right arrow-up caret-down caret-left caret-right caret-up check close content conversions-small conversions details edit grid help small-info error live magento minus move photo pin plus preview refresh search settings small-home stat subtract text trash unpin wordpress x alert case_deflection advanced-permissions keyword-detection predictive-ai sso

Building Faceted Search with jQuery Plugins

Faceted search is a way of browsing data by applying filters to items within a collection.

With wide adoption as a standard search UI element, facets are an expected feature in any e-commerce store.

Swiftype Faceted Search E-Commerce Store Example Screenshot


We will build a simple, read-only e-commerce product browsing page.

The tutorial uses:

The finished example can be found on GitHub.

Prerequisites

Before continuing, we'll examine our sample data.

If you are not familiar with the basics of the Swiftype API, please visit our API overview page.

In this example, product is the name of our DocumentType and it contains 50 Document records.

The schema for each product looks like this:

Example - Field schema for product
{:name => 'title', :type => 'string', :value => ... },
{:name => 'description', :type => 'text', :value => ... },
{:name => 'quantity', :type => 'integer', :value => ... },
{:name => 'category', :type => 'enum', :value => ... },
{:name => 'price', :type => 'float', :value => ... },
{:name => 'tags', :type => 'enum', :value => ... },
{:name => 'published_on', :type => 'date', :value => ... },
{:name => 'url', :type => 'enum', :value => ... }

Learn more about Site Search Engine schemas within our schema design guide.

Now, we can start building our example application.

Step 1: Create base HTML markup

Before we get to adding faceted search, we need to create a simple page, that does two things:

  1. Reference all of the necessary JavaScript libraries.
  2. Provide basic markup to serve as a foundation for the future steps in this tutorial.

Save the following files in a new folder...

From the Search jQuery plugin:

  • jquery.ba-hashchange.min.js
  • jquery.swiftype.search.js
  • search.css

From the Autocomplete jQuery plugin:

  • autocomplete.css
  • jquery.swiftype.autocomplete.js

Next, in the same folder, we will create two files named index.html and custom.css.

After that, we should make some changes to the index.html file:

<!DOCTYPE html>

<html>
  <head>
    <title>Swiftype Faceted Search E-Commerce Store Example</title>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
    <script type="text/javascript" src="http://twitter.github.com/hogan.js/builds/2.0.0/hogan-2.0.0.js"></script>
    <script type="text/javascript" src="jquery.swiftype.autocomplete.js"></script>
    <script type='text/javascript' src='jquery.ba-hashchange.min.js'></script>
    <script type="text/javascript" src="jquery.swiftype.search.js"></script>
    <link type="text/css" rel="stylesheet" href="autocomplete.css" media="all" />
    <link type="text/css" rel="stylesheet" href="search.css" media="all" />
    <link type="text/css" rel="stylesheet" href="custom.css" media="all" />
  </head>
  <body>
    <div class="container">

    <h1>Swiftype Faceted Search E-Commerce Store Example</h1>
    <form>
      <label>Search products</label>
      <input type='text' id='st-search-input' class='st-search-input' />
    </form>
    <div id="menu">
      <h3>Sorted by</h3>
      <a href="#" class="sort active" ><label>Relevance</label></a>
      <a href="#" class="sort" data-field="price" data-direction="asc"><label>Price</label></a>
      <a href="#" class="sort" data-field="published_on" data-direction="desc"><label>Date Added</label></a>

      <div id="facets">
        <div class="st-custom-facets">
          <h3>Prices</h3>
          <input type="checkbox" class="price-filter" name="price" data-type="range" data-from="0" data-to="5" id="under5">
          <label for="under5">Under $5</label><br />
          <input type="checkbox" class="price-filter" classname="price" data-type="range" data-from="0" data-to="10" id="under10">
          <label for="under10">Under $10</label><br />
          <input type="checkbox" class="price-filter" classname="price" data-type="range" data-from="0" data-to="25" id="under25">
          <label for="under25">Under $25</label><br />
          <input type="checkbox" class="price-filter" classname="price" data-type="range" data-from="50" data-to="*" id="over50">
          <label for="over50">Over $50</label>
        </div>
        <div class="st-dynamic-facets">
        </div>
      </div>
    </div>

    <div id="results">
      <div id="st-results-container">
        <span id="no-results">No products found</span>
      </div>
    </div>

    </div>

    <script type="text/javascript">
    </script>
  </body>
</html>

As you may have noticed, we are using Hogan.js as a JavaScript templating engine.

For custom.css, you can either style it yourself or grab our example stylesheet.

Now, open index.html in your browser.

You should see header text and a search box on the left-hand side of the page.

On the right-hand side, you should see a panel with a few filters.

Keep in mind that we have not yet wired up any functionality!

Step 2: Enable both jQuery Plugins

Now we will add working search with autocomplete functionality to our page.

First, let's begin by adding autocomplete functionality.

Inside of our <script> tag, initialize the Autocomplete jQuery plugin.

Example - Autocomplete jQuery initialized
$('#st-search-input').swiftype({
  engineKey: 't2s8T3sUKx4jJoebs73L'
});

Try typing into the search box -- we have autocomplete results!

Next, initialize the Search jQuery plugin.

Example - Search jQuery initialized
var resultTemplate = Hogan.compile([
  "<div class='product'>",
    "<h2>Faceted Search with Swiftype</h2>",
    "<div>Quantity: </div>",
    "<div>Category: </div>",
    "<div>Price: $</div>",
    "<div>Tags: </div>",
    "<div>Published: </div>",
  "</div>"
].join('') );

var customRenderFunction = function(document_type, item) {
  var date = new Date(item['published_on']),
  data = {
    title: item['title'],
    quantity: item['quantity'],
    price: item['price'],
    category: item['category'],
    tags: item['tags'],
    published_on: [date.getMonth(), date.getDate(), date.getFullYear()].join('/')
  };
  return resultTemplate.render(data);
};

$('#st-search-input').swiftypeSearch({
  resultContainingElement: '#st-results-container',
  engineKey: 't2s8T3sUKx4jJoebs73L',
  renderFunction: customRenderFunction
});

In this snippet, renderFunction takes a function that customizes the way results are displayed.

In our case customRenderFunction is the function used to render a template for each individual result.

The resultTemplate variable contains a Hogan.js template.

The template is populated via the customRenderFunction function before being rendered onto the page.

We should now see both search and autocomplete.

Step 3: Enable Sorting

As with any real e-commerce website, it is important to be able to sort the data.

Learn more about sorting with the Site Search API.

Looking through the information we have for each product, let's pick a few columns to sort by:

  • Relevancy (default)
  • Price
  • Date Added

As you may have noticed, we already have links that switch between different sorting options on the right-hand side of the screen. To make links functional, let's add the following code to our <script> section:

Example - Enable sorting
var searchConfig = {
  sort: {
    field: undefined,
    direction: undefined
  }
};

var reloadResults = function() {
  $(window).hashchange();
};

var readSortField = function() {
  return { products: window.searchConfig.sort.field };
};

var readSortDirection = function() {
  return { products: window.searchConfig.sort.direction };
};

$('.sort').on('click', function(e){
  e.preventDefault();
  // Visually change the selected sorting order
  $('.sort').removeClass('active');
  $(this).addClass('active');
  // Update sorting settings
  window.searchConfig.sort.field = $(this).data('field');
  window.searchConfig.sort.direction = $(this).data('direction');

  reloadResults();
});

$('#st-search-input').swiftypeSearch({
  // code from the previous step
  sortField: readSortField,
  sortDirection: readSortDirection
});

There is a lot going on, so let's review it step by step.

We...

  1. Created the searchConfig variable to hold search query settings, which are limited to field and direction for sort at the moment. The way the field gets set is by attaching an event-handler to HTML links with the sort class. These events fire when a user clicks a link to change the sorting order.

  2. Added the following two options to the swiftypeSearch initialization: sortField and sortDirection to define the sort order on each request. We use the readSortField and readSortDirection functions to access the sort properties from the searchConfig variable by accessing window.searchConfig.sort.field and window.searchConfig.sort.direction.

  3. Wrote a function called reloadResults, which reloads and updates the results.

Go ahead and try to sort the results!

Step 4: Enable Filtering

Another important feature for a good e-commerce website is the ability to filter the inventory.

Learn more about filtering with the Site Search API.

In our example, there are many ways we can filter the data:

  • by when it was added (today, last week, etc)
  • by quantity remaining
  • by price range.

In this tutorial we will filter our results by price ranges.

We will have 4 price categories to filter by:

  • Under $5
  • Under $10
  • Under $25
  • Over $50

To implement these filters, let's again add some code to our <script> tag:

Example - Enable filtering
var searchConfig = {
  // code from the previous step
  price: {
    from: undefined,
    to: undefined
  }
};

var readFilters = function() {
  return {
    products: {
      price: {
        type: 'range',
        from: window.searchConfig.price.from,
        to: window.searchConfig.price.to
      }
    }
  }
}

$('#st-search-input').swiftypeSearch({
  // code from the previous step
  filters: readFilters,
});

$('.price-filter').on('click', function(e){
  if ($(this).attr('checked')) {
    // Visually update the checkboxes
    $('.price-filter').attr('checked', false);
    $(this).attr('checked', true);
    // Update the search parameters
    window.searchConfig.price.from = $(this).data('from');
    window.searchConfig.price.to = $(this).data('to');
  } else {
    window.searchConfig.price.from = undefined;
    window.searchConfig.price.to = undefined;
  }
  reloadResults();
})

First, we expanded the searchConfig variable with another key named price.

It is a hash with the keys from and to.

These keys are going to be used to keep track of the values that we are going to filter the price column by.

We also added another event-handler for click events on links with the price-filter class.

In addition to updating some visuals, this function updates the price values inside searchConfig.

We also made another change to swiftypeSearch initialization: this time we've added the filters option, which takes the readFilters function. Calling readFilters returns a hash with filter settings and, in our case, specifies price range to filter by.

Filters should be fully functional now... Selecting the "Under $5" category will make the list shrink!

Ahh, the last step...

In our case the behavior for faceted search is going to be as follows:

  1. Typing a query (such as "products") will load up a list of results as well as facets. For example, in the category facet we would expect to see "books (3)" where "books" is the category and "3" is number of available books.

  2. When we click on one of the facets, we would like to add that category to our filters and, subsequently, for the search result set to shrink. For example, if we clicked "books (3)", we would expect to see only the 3 books in that category. Additionally, if we select one of the tags facets, we would expect to see the number of products shrink to fewer than before.

  3. Clicking "Clear All" will clear any selected facets and increase the number of products shown.

Let's add faceted search, now:

Example - How to Enable Faceted Search
var searchConfig = {
  // code from previous steps
  facets: {},
};

var $facetContainer = $('.st-dynamic-facets');

$('#st-search-input').swiftypeSearch({
  // code from previous steps
  facets: { products: ['category', 'tags'] },
  postRenderFunction: bindControls
});


var bindControls = function(data) {
  var resultInfo = data['info'],
  facets = '';

  $.each(resultInfo, function(documentType, typeInfo){
    $.each(typeInfo.facets, function(field, facetCounts) {
      facets += ['<div class="facet"><h3>', field, '</h3></div>'].join('')
      $.each(facetCounts, function(label, count) {
        var status = "",
        id = encodeURIComponent(label).toLowerCase();
        if (window.searchConfig.facets[field] && window.searchConfig.facets[field].indexOf(label) > -1) {
          status = ' checked="checked"';
        }

        facets += '<input type="checkbox"' + status + ' name="' + field + '" value="' + label + '" id="' + id + '"> <label for="' + id + '">' + label + ' (' + count + ')</label><br/>';
      });
      facets += '<a href="#" class="clear-selection" data-name="' + field + '">Clear all</a>'
    });
    $facetContainer.html(facets);
  });
};

$facetContainer.on('click', 'input', function(e) {
  window.searchConfig.facets = {}; // Set the hash to empty
  $('.st-dynamic-facets input[type="checkbox"]').each(function(idx, obj) {
    var $checkbox = $(obj),
    facet = $checkbox.attr('name');
    if(!window.searchConfig.facets[facet]) { // if facets are null or undefined, put empty array for the category
      window.searchConfig.facets[facet] = [];
    }
    if($checkbox.attr('checked')) { // add ids to array for the category
      window.searchConfig.facets[facet].push($checkbox.attr('value'));
    }
  });

  reloadResults();
});

$facetContainer.on('click', 'a.clear-selection', function(e) {
  e.preventDefault();
  var name = $(this).data('name');
  $('input[name=' + name + ']').attr('checked', false);
  window.searchConfig.facets[name] = [];

  reloadResults();
});

var readFilters = function() {
  return {
    products: {
      category: window.searchConfig.facets['category'],
      tags: window.searchConfig.facets['tags'],
      price: {
        type: 'range',
        from: window.searchConfig.price.from,
        to: window.searchConfig.price.to
      }
    }
  };
};

Understandably, with this much code to digest, we should take it step by step.

We added another property key to searchConfig:

var searchConfig = {
  // code from previous steps
  facets: {},
};

The facets object stores facet settings.

We selected the HTML element that will contain our facets and set it to a variable for easy future reference:

var $facetContainer = $('.st-dynamic-facets');

We also added two more options to our swiftypeSearch initialization call:

facets: { products: ['category', 'tags'] },
postRenderFunction: bindControls

The facets option specifies DocumentType and columns to give you a count of results in that column.

Learn more about faceting with the Site Search API.

In our case, we are going to get facets for category and tag columns.

The postRenderFunction takes a function that is called after results are returned from Site Search.

Our example function is named bindControl and will be responsible for rendering the facets to the page:

var bindControls = function(data) {
  var resultInfo = data['info'],
  facets = '';
}

Two things are created in this step.

  1. The resultInfo variable, which gets assigned the info key from the response.
  2. The facets variable that we will use to construct HTML tags to populate $facetContainer, which we defined earlier.
$.each(resultInfo, function(documentType, typeInfo){
  $.each(typeInfo.facets, function(field, facetCounts) {
    facets += ['<div class="facet"><h3>', field, '</h3></div>'].join('')
  });
});

We iterate through collections of data down to the facet information.

The field is either tags or category, and facetCounts contains information about each facet and its count.

We are going to take this data and add the header tag to the facets variable:

$.each(facetCounts, function(label, count) {
  var status = "",
  id = encodeURIComponent(label).toLowerCase();
  if (window.searchConfig.facets[field] && window.searchConfig.facets[field].indexOf(label) > -1) {
    status = ' checked="checked"';
  }

  facets += '<input type="checkbox"' + status + ' name="' + field + '" value="' + label + '" id="' + id + '"> <label for="' + id + '">' + label + ' (' + count + ')</label><br/>';
});

We iterate through facetCounts for a given column, constructing <input> and <label> elements for each facet.

Note that the status variable is going to be set to checked for the facets that are currently part of filter.

In other words, since facets are re-drawn on each query change, we need to keep track of what facets have been selected.

When we are done iterating through the collection of facets, we will add a "Clear All" link and set the HTML contents of $facetContainer to the content of the facets variable:

  facets += '<a href="#" class="clear-selection" data-name="' + field + '">Clear all</a>'
});
$facetContainer.html(facets);

We also include two event-handlers:

$facetContainer.on('click', 'input', function(e) {
  window.searchConfig.facets = {}; // Set the hash to empty
  $('.st-dynamic-facets input[type="checkbox"]').each(function(idx, obj) {
    var $checkbox = $(obj),
    facet = $checkbox.attr('name');
    if(!window.searchConfig.facets[facet]) { // if facets are null or undefined, put empty array for the category
      window.searchConfig.facets[facet] = [];
    }
    if($checkbox.attr('checked')) { // add ids to array for the category
      window.searchConfig.facets[facet].push($checkbox.attr('value'));
    }
  });

  reloadResults();
});

This one responsible for updating filter values inside searchConfig on each facet click.

$facetContainer.on('click', 'a.clear-selection', function(e) {
  e.preventDefault();
  var name = $(this).data('name');
  $('input[name=' + name + ']').attr('checked', false);
  window.searchConfig.facets[name] = [];

  reloadResults();
});

This one is responsible for clearing out facet selection after clicking the "Clear All" link.

Lastly, we updated readFilters to include category and tags as filters:

var readFilters = function() {
  return {
    products: {
      category: window.searchConfig.facets['category'],
      tags: window.searchConfig.facets['tags'],
      price: {
        type: 'range',
        from: window.searchConfig.price.from,
        to: window.searchConfig.price.to
      }
    }
  };
};

Let's open your page and try to search for "product" again.

You should see a list of facets populated on the right-hand side of the page.

Clicking a facet should narrow down the search and update other facets.

Try it yourself!

If, for whatever reason, your code is not working right, feel free to refer to our working example.

Stuck? Looking for help? Contact support or check out the Site Search community forum!