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

Faceted Search Guide

(Using jQuery)

Faceted search is a method of browsing data. They apply 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 documents.

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.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 src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.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.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'
        placeholder="Search by typing (ex. 'product') and pressing enter"
        aria-label="Search by typing (ex. 'product') and pressing enter"
      />
    </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 add autocomplete functionality.

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

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

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

Next, we'll initialize the Search jQuery plugin. As a heads up, we'll continue to modify the settings we're passing into swiftypeSearch's options throughout the tutorial, so you'll want to have this at the bottom of your <script> tag.

Example - Search jQuery initialized
$('#st-search-input').swiftypeSearch({
  resultContainingElement: '#st-results-container',
  perPage: 12,
  engineKey: 't2s8T3sUKx4jJoebs73L',
});

Now, let's set up custom rendering for our search results. Place this code at the top of your <script> tag.

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

var resultTemplate = Hogan.compile([
  "<div class='product'>",
    "<h2>Faceted Search Guide</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);
};

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.

And as mentioned before, we'll now need to update our previously-initialized search settings with our new render function.

Example - Updated swiftypeSearch settings
$('#st-search-input').swiftypeSearch({
  // ...  code from the previous step
  renderFunction: customRenderFunction,
});

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 the top of your <script> section:

Example - Initialize the searchConfig object
var searchConfig = {
  sort: {
    field: undefined,
    direction: undefined
  },
};

The searchConfig object holds search query settings, which are limited to field and direction for sort at the moment (we'll add more later in the tutorial).

Let's continue to make our sort links functional by adding the following code below the searchConfig variable:

Example - Enable sorting
var reloadResults = function() {
  Swiftype.reloadResults();
};

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

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

We...

  1. Attached an event-handler to HTML links with the sort class, which fires when a user clicks a link to change the sorting order. This event updates our searchConfig sort field.

  2. Created the readSortField and readSortDirection functions to access the sort properties from the searchConfig object by accessing window.searchConfig.sort.field and window.searchConfig.sort.direction.

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

Now, let's update our existing swiftypeSearch settings to see this in action.

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

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 start by updating our searchConfig:

Example - Updated searchConfig object
var searchConfig = {
  // ... code from the previous step
  price: {
    from: undefined,
    to: undefined
  },
};

This expanded the searchConfig object 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.

Let's again continue by adding some code to our <script> tag, under our searchConfig object:

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

$('.price-filter').on('click', function(e){
  if (this.checked) {
    // Visually update the checkboxes
    $('.price-filter').each(function() {
      this.checked = false;
    });
    this.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();
})

We've 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.

Calling readFilters returns a hash with filter settings and, in our case, specifies price range to filter by.

Now, let's update our existing swiftypeSearch settings with the filters option, which takes the readFilters function.

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

Filters should be fully functional now... Selecting the "Under $10" 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 "product") 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 start by updating searchConfig:

Example - Updated searchConfig object
var searchConfig = {
  // ... code from the previous step
  facets: {},
};

We just added another property key to searchConfig. The facets object stores facet settings.

Let's add faceted search, now:

Example - Enable Faceted Search
var $facetContainer = $('.st-dynamic-facets');

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(obj.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 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 created a bindControl function, which is called after results are returned from Site Search, and 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 + ']').each(function() {
    this.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
      }
    }
  };
};

Now, let's update our existing swiftypeSearch settings.

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. We've passed in bindControl (described above), which will be responsible for rendering the facets to the page.

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

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!