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
Swiftype Documentation / tutorials: Faceted Search with Swiftype

Building Faceted Search with Swiftype JavaScript Plugins

Swiftype Faceted Search E-Commerce Store Example Screenshot

In this tutorial we will walk through the steps to build simple, read-only e-commerce product browsing page. The tutorial is designed to demonstrate the basics of faceted search using Swiftype via the Swiftype Autocomplete jQuery and Swiftype Search jQuery plugins. The finished example can be found on GitHub.

What is Faceted Search?

Faceted search, also known as faceted navigation, is a way of browsing data by applying filters to the items in the collection. With wide adoption as a standard UI element on many modern sites, faceted search is becoming an expected feature for any e-commerce store.

Prerequisites

In this example, we will be working with sample data that is provided. Before continuing, however, let's dig a bit deeper into the data itself. 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 => ... }
  

To learn more about Swiftype search engine schemas, refer to our schema design tutorial. Now, we can start building our example application.

Step 1: Create base HTML markup

Before we get to adding faceted search features themselves, we need to create a simple page with references to all of the necessary JavaScript libraries, as well as basic markup to serve as a foundation for the future steps in this tutorial. To do that, save the following files in a new folder:

Next, in the same folder, we will create two files named index.html and custom.css. Let's go ahead and make some changes to the index.html file first:

Example - index.html HTML markup
<!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. To learn more about Hogan.js, click here.

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

When you open your copy of 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

The goal for this step is to add working search with autocomplete functionality to our page. Let's begin by adding autocomplete functionality. Inside of our <script> tag, initialize the Swiftype Autocomplete jQuery plugin.

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

Now, typing into the search box should yield autocomplete results. Next, initialize the Swiftype Search jQuery plugin.

Example - Swiftype Search jQuery initialized
var resultTemplate = Hogan.compile([
  "<div class='product'>",
    "<h2>{{title}}</h2>",
    "<div>Quantity: {{quantity}}</div>",
    "<div>Category: {{category}}</div>",
    "<div>Price: ${{price}}</div>",
    "<div>Tags: {{tags}}</div>",
    "<div>Published: {{published_on}}</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. Note that the resultTemplate variable contains a Hogan.js template, which gets populated with data collected inside the customRenderFunction function before rendering onto the page.

At this point both autocomplete as well as search results should show up on the page.

If at any point you feel like you are no longer following along or would like to see the final result for reference, please refer to the completed index.html

Step 3: Enable Sorting

As with any real e-commerce website, it is important to be able to sort the data. For an overview of sorting with the Swiftype API, refer to our documentation. 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 in this step, so let's review it step by step. First, we 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.

We also went ahead and 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.

Lastly, to update the results, we created a function called reloadResults, which reloads the results. Go ahead and try to sort the results!

In readSortField or readSortDirection and elsewhere, "products" is the slug of DocumentType containing the Documents we are searching through. Be sure to change it if you're updating this code to fit your own needs.

Step 4: Enable Filtering

Another important feature for a good e-commerce website is the ability to filter the inventory. To learn more about filtering with the Swiftype API, refer to our documentation. In our example, there are many ways we can filter the data: by when it was added (today, last week, etc), by quantity remaining, or by price range. In this tutorial we will filter our results by price ranges. As you can probably tell, 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, which 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 of the visuals on the page, this function is responsible for updating 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, so let's test it by selecting the "Under $5" category and seeing the list shrink.

Step 5: Enable Faceted Search

Finally, let's add faceted search. 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 I would expect to see "books (3)" where "books" is the category and "3" is number of available books.

  2. When I click on one of the facets, I would like to add that category to our filters and, subsequently, for the search result set to shrink. For example, if I clicked "books (3)", I would expect to see only the 3 books in that category. Additionally, if I select one of the tags facets, I 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.

Now let us implement the feature.

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. First, we added another property key to searchConfig:

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

facets stores facet settings. Next, we selected 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. To learn more about faceting, refer to the documentation. In our case, we are going to get facets for category and tag columns. postRenderFunction takes a function that is called after results are returned from Swiftype. In our case, this function is named bindControl and will be responsible for rendering the facets to the page. Let's review the code there:

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

Two things happen in this step: We create the resultInfo variable, which gets assigned the info key from the response. We also create 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('')

Next, we iterate through collections of data down to the facet information. In our tutorial example, field is either tags or category, and facetCounts contains information about all individual facets and counts. 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/>';
});

Now, we are iterating through facetCounts for a given column and constructing <input> and <label> element 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 a way to keep track of what facets have been selected.

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

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

We also added 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();
});

is 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();
});

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 - this time 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.