Integrating your Kapsule

Now that you have built your Kapsule, you can start integrating it in your own application!

We will guide you through this process, going through each component needed to interact with a Kapsule.

If you'd rather try things directly, you check out the Starter Kit automatically generated in your account.

Components

Here are the main components of a search engine powered by Kapsule.

First, we have a search bar, to enter terms to search. Then there are the search results themselves, showing all the fields toggled for display in the configuration phase. There is also the optional pagination, that can be opted out if you prefer infinite scrolling. Finally, we have the facets, usually going in pair with the active filters.

/static/docs/img/schema.svg

UI/UX

Search results

/static/docs/img/results.svg

A basic but well established trend is to stack the results below the search bar. Yet, nothing stops your from showing your creativity and explore new ways to display the search results. In any case, we will assume that results are a list of individual entry from the document store.

Facets

/static/docs/img/facets.svg

Facets are usually displayed on the left hand side of the screen. Kapsule handles the logic behinds the facets along with the behavior on selecting or unselecting facet values.

Filters

/static/docs/img/filters.svg

When a user clicks on a facet, its state changes to selected, and the corresponding filter is added internally. In order to offer feedback on the active filters applied, it is possible to display the Kapsule's active filters. Filters applied to the query are usually displayed below or next to the search bar.

Starter Kit

Once you have built your very own Kapsule, you can preview what the integration may look like. While this preview is very useful to test your Kapsule settings, and finetune your configuration, it's not such a great starting point for integrating your own Kapsule.

To start integrating, you can check the Starter Kit, accessible from the Kapsule configuration page or preview.

You can try, and most importantly download this webpage to get started with the integration. Both the Preview and Starter Kit adapt to your kapsule and display settings.

Once downloaded you will have a zip file with:

  • index.html: Default template to hold your first Kapsule.
  • syle.css : Minimal styling to get started.
  • kapsule.js : Your own Kapsule
  • main.js : Everything needed to handle the Kapsule display. Its content is explained in the section below.

Integrating components

Search bar

A few basic behaviors are linked to the search bar.

  • kapsule.search() shall be called upon search validation, or enter key pressed.
    • Note that for Kapsules configured with instant search, kapsule.search() must be called for each key pressed.
  • kapsule.reset() shall be called upon reset button clicked.

Here is a short snippet to start searching when pressing enter in the search input.

var launchSearch = function () {
    // Grab input search terms
    const terms = document.querySelector('#search-input').value

    // Do not search for empty text
    if (!terms) {
        return
    }

    kapsule.search({ terms: terms })
}

document.addEventListener('DOMContentLoaded', () => {

    // Bind search event (Classic search)
    // Bind click on search button
    document.querySelector('#search-button').addEventListener('click', launchSearch)

    // Bind enter key pressed
    document.querySelector('#search-input').addEventListener('keyup', function (event) {
        if (event.key === 'Enter') {
            launchSearch()
        }
    })
}

In the snippet below, this is what it can look like when using instant search, i.e. we search for each key pressed, when the number of characters is bigger than what was set during configuration.

document.addEventListener('DOMContentLoaded', () => {

    // Bind search event (Instant search)
    document.querySelector('#search-input').addEventListener('keyup', function (event) {
        if (document.querySelector('#search-input').value.length >= window.kapsule.settings.minChars) {
            launchSearch()
        }
    })
}

Search results

The first thing to do is to define your own displayResults(searchResults) function. Then, upon calling kapsule.init(), you shall pass that function as the result display callback.

The displayResults function will receive searchResults as a parameter, holding :
  • The current page.
  • The number of pages, totalPages.
  • The number of results, totalResults.
  • The time spent processing the query.
  • An array of results, data, containing all the fields configured for display.

Here, we are only using the data parameters to display the results. We will be using the other parameters later on.

var resultsToPage = function (searchResult) {
    // Build list of results
    for (const result of searchResult.data) {
        const ul = document.createElement('ul')
        for (const field in result) {
        const li = document.createElement('li')
        li.innerText = field + ': ' + result[field]
        ul.appendChild(li)
        }
        document.querySelector('#result-section').appendChild(ul)
    }
}

var displayResults = function (searchResults) {

    // Clear the previous results
    document.querySelector('#result-section').innerHTML = ''

    // Add the results to the page
    resultsToPage(searchResults)
}

document.addEventListener('DOMContentLoaded', () => {

    kapsule.init({
        displayResults: displayResults
    })
}

Each time results need to be updated, let it be due to a new search, a reset, ... the Kapsule will make a call to the displayResults callback to refresh what is shown on screen.

This simplifies the integration of the Kapsule a lot, you only need to choose how to display the results. When to update them is completely handled internally in the Kapsule.

Pagination

All interactions with pagination buttons shall be bound to kapsule.getPage(pageNumber).

As described in the API, pageNumber can either be:
  • a number : the page number to call for, it will update results for this page
  • "first" : same as 1
  • "previous" : currentPage-1
  • "next" : currentPage+1
  • "last" : maxPages

Please note that you should handle the display of the pagination in the displayResults callback. Building on the previous example, here is what it can look like.

var displayResults = function (searchResults) {

    // Clear the previous results
    document.querySelector('#result-section').innerHTML = ''

    // Update pagination
    updatePagination(searchResults)

    // Add the results to the page
    resultsToPage(searchResults)
}

var updatePagination = function (searchResults) {
    // if results does not have totalPages (initial state) or no length hide it
    if (!(Object.prototype.hasOwnProperty.call(searchResults, 'totalPages')) || searchResults.totalPages <= 1) {
        document.querySelector('#pagination-wrapper').style.display = 'none'
    }
    else {
        document.querySelector('#pagination-wrapper').style.display = 'block'
    }

    // We want 2 pages each side of current page
    const radius = 2
    let start = searchResults.page - radius
    let end = searchResults.page + radius

    // Make sure we do not go outside the radius boundaries
    if (searchResults.page <= radius) {
        start = 1
    }
    if (searchResults.page >= searchResults.totalPages - radius) {
        end = searchResults.totalPages
    }

    // Show/hide buttons if there pages before current page
    if (searchResults.page > 1) {
        document.querySelector('.first-page').style.removeProperty('display')
        document.querySelector('.previous-page').style.removeProperty('display')
    }
    else {
        document.querySelector('.first-page').style.display = 'none'
        document.querySelector('.previous-page').style.display = 'none'
    }

    // Show/hide buttons if there pages after current page
    if (searchResults.page < searchResults.totalPages) {
        document.querySelector('.last-page').style.removeProperty('display')
        document.querySelector('.next-page').style.removeProperty('display')
    }
    else {
        document.querySelector('.last-page').style.display = 'none'
        document.querySelector('.next-page').style.display = 'none'
    }

    // Add numbered pagination buttons
    for (let i = start; i <= end; i++) {
        // Create span and add classes
        const page = document.createElement('span')
        page.classList.add('list-options-page-link')
        if (i === searchResults.page) {
        page.classList.add('checked')
        }
        page.innerHTML = i
        page.setAttribute('id', 'page-' + i)

        // add click event to page number
        page.addEventListener('click', function (event) {
        const pageId = parseInt(event.target.id.split('-')[1])
        window.kapsule.getPage(pageId)
        })

        // Add button to DOM
        document.querySelector('#pages-wrapper').appendChild(page)
    }
}

We just have to bind the "first" and "last" page buttons once :

var setUpPagination = function () {
    document.querySelector('.first-page').addEventListener('click', function () {
        window.kapsule.getPage('first')
    })
    document.querySelector('.last-page').addEventListener('click', function () {
        window.kapsule.getPage('last')
    })
    document.querySelector('.next-page').addEventListener('click', function () {
        window.kapsule.getPage('next')
    })
    document.querySelector('.previous-page').addEventListener('click', function () {
        window.kapsule.getPage('previous')
    })
}

document.addEventListener('DOMContentLoaded', () => {

    // Bind pagination events
    setUpPagination()
}

Infinite scrolling

If you would rather use infinite scrolling, you just need to call kapsule.getPage("next", append=true) when reaching the bottom of the screen.

It will update the results with the next page and tell the callback that it should append results instead of replacing them.

Note that it is up to you to handle the searchResults.appendResults flag received in displayResults. Here is a quick refined example of what we achieved previously.

var displayResults = function (searchResults) {
    if (!searchResults.appendResults) {
        // Clear the previous results only when told to.
        document.querySelector('#result-section').innerHTML = ''
    }

    // Add the results to the page
    resultsToPage(searchResults)
}

document.addEventListener('DOMContentLoaded', () => {

    // Bind event when scrolling at the bottom of the page with loading more results.
    window.addEventListener('scroll', function () {
        if (window.scrollY > (document.body.offsetHeight - window.outerHeight)) {
            // Get more results
            kapsule.getPage('next', append = true)
        }
    })
}

Facets

Facets are handled in a similar way than the results. Upon initialization of the Kapsule, one must provide a callback function, called when the display of facets needs to be updated.

document.addEventListener('DOMContentLoaded', () => {

    kapsule.init({
        displayResults: displayResults,
        displayFacets: displayFacets
    })
}
The displayFacets function will receive a list of facets, and for each facet:
  • The jsonPath of the field.
  • And for each value of this facet :
    • The value as a string.
    • The number of time this value is present (count).
    • A flag whether the value is selected or not.
    • An update callback function to call on click event.

What is left to do is display the facets, and bind the click event to the underlying update callback for each facet value :

var createFacetValueElement = function (facet, facetValue, no) {
    // Create list element
    const facetValueElement = document.createElement('span')
    facetValueElement.classList.add('facet-value')

    // Create label
    const labelElement = document.createElement('label')
    labelElement.setAttribute('for', facet.jsonPath + '-' + no)
    labelElement.innerText = facetValue.value + ' (' + facetValue.count + ')'

    // Create input checkbox
    const inputElement = document.createElement('input')
    inputElement.setAttribute('type', 'checkbox')
    inputElement.setAttribute('id', facet.jsonPath + '-' + no)

    // Add checked property if facet is selected
    if (facetValue.selected) {
        inputElement.checked = true
    }

    // Bind click event with associated update callback
    inputElement.addEventListener('click', facetValue.update)

    // Build html
    labelElement.appendChild(inputElement)
    facetValueElement.appendChild(labelElement)

    return facetValueElement
}

var createFacetElement = function (facet) {
    // Create one div per facet
    const facetElement = document.createElement('div')
    facetElement.classList.add('facet-group')

    // Create associated header
    const headerElement = document.createElement('HEADER')
    headerElement.innerText = facet.jsonPath
    facetElement.appendChild(headerElement)

    // Create associated facet values
    const facetValueWrapper = document.createElement('ul')
    facetValueWrapper.classList.add('facet-group-facets')

    // For each facet value,
    for (let i = 0; i < facet.values.length; i++) {
        // Create and add to DOM facet value element
        facetValueWrapper.appendChild(createFacetValueElement(facet, facet.values[i], i))
    }
    facetElement.appendChild(facetValueWrapper)

    return facetElement
}

var displayFacets = function (facets) {
    // clear facet sections
    document.querySelector('#facets-section').innerHTML = ''

    // for each facet
    for (let i = 0; i < facets.length; i++) {
        const facet = facets[i]

        // if there is at least one facet value for this facet
        if (facet.values.length > 0) {
        // Create and append facet to DOM
        document.querySelector('#facets-section').appendChild(createFacetElement(facet))
        }
    }
}

Filters

Display of active filters is also handled by the Kapsule. Once given a callback function to update the display, the Kapsule will call it whenever the list of active filters changes.

document.addEventListener('DOMContentLoaded', () => {

    kapsule.init({
        displayResults: displayResults,
        displayFacets: displayFacets,
        displayFilters: displayFilters
    })
}
The displayFilters function will receive a list of active filters, and for each filter:
  • The jsonPath of the field.
  • The operation applied (AND, OR...).
  • An array of values used for filtering.
  • An update callback function to call on click event.

Here is a snippet rendering the filters and binding the update callback.

var createFilterElement = function (filter) {
    // Create filter element
    const filterElement = document.createElement('li')
    filterElement.classList.add('facet-filter')

    // Add the name of the filter (its json path)
    const nameElement = document.createElement('strong')
    nameElement.innerText = filter.jsonPath + ': '
    filterElement.appendChild(nameElement)

    // Create the container for each filtering value
    const valueElements = document.createElement('span')
    valueElements.classList.add('filters-container')

    // Iterate through the operators and values applied to this filter
    const operations = filter.operations
    for (const operator in operations) {
        for (let i = 0; i < operations[operator].length; i++) {
            // Get the filter item
            const filterItem = operations[operator][i]

            // Avoid a trailing comma
            var filterItemText = filterItem.value
            if (i > 0) {
                filterItemText = ', ' + filterItem.value
            }

            // Create checkbox to remove filter
            const crossElement = document.createElement('input')
            crossElement.setAttribute('type', 'checkbox')
            crossElement.checked = true

            // Create text for filter
            const valueElement = document.createElement('span')
            valueElement.classList.add('highlight')
            valueElement.innerText = filterItemText
            valueElement.appendChild(crossElement)

            // Bind click event to Kapsule internal callback
            valueElement.addEventListener('click', filterItem.update)

            // Add it to the list of filtering values
            valueElements.appendChild(valueElement)
        }
    }

    // Add the list of values used for this filter
    filterElement.appendChild(valueElements)

    return filterElement
}


var displayFilters = function (filters) {
    // Clear previous filters
    document.querySelector('#filters-section').innerHTML = ''

    // For each active filter
    for (var filter of filters) {
        // Append it to the filter section
        document.querySelector('#filters-section').appendChild(createFilterElement(filter))
    }
}