How to Add InstantSearch Analytics Widgets in Typesense

Building an e-Commerce with multi-search functionalities.

How to Add InstantSearch Analytics Widgets in Typesense

Introduction

An Instant Search Analytics Widget is an essential feature to add to your list of todos for a smooth user experience when you're building a data-heavy website like Unsplash or Undraw or an e-commerce website like Alibaba that deals heavily with real-time searching, sorting, filtering, and pagination of items.

This is what a typical Instant Search Analytics Widget looks like on Alibaba.

Alibaba instant search widget

The Instant Search Analytics Widget as shown above provides a visual representation of how many products are available in each country, as well as filtering choices for consumers.

We’ll be implementing the Instant Search Analytics Widget in this article using Typesense and InstantSearch.js. You can check out the PHP, Vue, or Astro versions of the implementation.

What is InstantSearch.js

InstantSearch.js is an open-source user interface library for vanilla Javascript that enables frontend developers to easily develop a scalable search user interface in their application using widgets.

PS: InstantSearch.js is maintained by Algolia.

What we’ll Build

In this article we’ll develop an e-commerce website with filtering, sorting and data grouping functionalities using Typesense with InstantSearch.js.

Below is the demo of what our e-commerce website will look like at the end of this article.

Demo

Setting up Typesense Cloud

The easiest way to integrate Typesense into your project is by using the cloud option. This option only takes a few minutes to set up compared to running Typesense yourself.

This section will guide you through the process of setting up and running your Typesense cloud instance.

Visit the Typesense Cloud website and click “Login With GitHub”, then GitHub will handle the rest.

Login on typesense by clicking the login with github button

After you've successfully created your account. You'll be redirected to the new cluster page, where you can start building your cluster.

In this example, I'll keep the default cluster configuration (you can scale this up later).

creating a cluster on typesense

Click the Launch button when you’re done and Typesense will take care of the rest.

It will take a few minutes for your cluster to be fully built, after which we can generate the API token for our project.

wait for the typesense cluster to be generated

When your cluster is ready for use. Click on the “Generate API Keys” buttons to download your cluster credentials.

generate API key on typesense

The downloaded .txt file will contain your Typesense Cloud credentials in this format:

=== Typesense Cloud: eu2ktobrcxxxxxx ===

Admin API Key:
O7x1QWQW9gfFxxxxxxxxx

Search Only API Key:
y9bwIwwEnWYxxxxxxxxxx

Nodes:
- eu2ktobrcxxxxxx-1.a1.typesense.net [https:443]

Keep your credentials private and save the .txt file because we'll be needing it to set up our project later in this tutorial.

Creating New Collection Schema

We’ll get our e-commerce products from the fakestoreapi.com/products endpoint, which returns 20 records for us to test with.

You can check out the records on Hoppscotch by clicking here.

testing fakestoreapi on hoppscotch

Let's look at what each product record contains so that we can build our schema with them.

{
   "id": 1,
   "title": "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
   "price": 109.95,
   "description": "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your every day",
   "category": "men's clothing",
   "image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
   "rating": {
     "rate": 3.9,
     "count": 120
   }
}

From the above record, we can define our collection schema data types as follows:

  1. title - string
  2. price - float
  3. description - string
  4. category - string
  5. image - string

Now that we know what our dataset should look like, we can proceed to create our collection.

You can see a collection as a table in a relational database.

From your dashboard sidebar:

  1. Click on “New Collection”

  2. Next, we’ll define what our collection should look like.

Update the Json editor with the following lines of JSON objects:

{
  "name": "products",
  "fields": [
    {
      "name": "title",
      "type": "string",
      "facet": false
    },
    {
      "name": "price",
      "type": "float",
      "facet": true
    },
    {
      "name": "description",
      "type": "string",
      "facet": false
    },
    {
      "name": "category",
      "type": "string",
      "facet": true
    },
    {
      "name": "image",
      "type": "string",
      "facet": false
    }
  ],
  "default_sorting_field": "price"
}

From the JSON above our collection was named "products", and we have set the names and data types of each product field in our collection. The field with the facet set to true will be used as a search query by Typesense. Lastly, we’re instructing Typesense to sort the products by default based on their pricing using default_sorting_field.

Click on the "Create Collection" button when you’re done.

creating collection on typesense

After setting your schema, you’ll be notified when your collection has been created.

Installing InstantSearch.js

This section outlines how to set up the InstantSearch.js project.

Firstly, you need to have Node.js installed on your system before you can install InstantSearch.js.

Run node --``version in your terminal to check if Node.js is installed on your system already.

If not installed, you can download the latest version of Node.js from here.

This tutorial makes use of Node version 16.13.2.

Next, follow the outlined steps below to install InstantSearch.js on your system.

In your terminal where you want your project to live:

  1. Run npx create-instantsearch-app unclebigbay-store
    • Enter your app name (default )
    • Select InstantSearch.js template
    • Select the latest stable version
    • Skip Application ID (latency)
    • Skip API key (we’ll set this up later)
    • Enter your collection name (products) as your Index name (instant_search)
    • Leave Attributes display as none
    • Lastly, select Dynamic Widgets

Your terminal should look something like this after a successful installation.

installing instant search js

  1. Run cd <name-of-your-project> to navigate into your app folder.

Setting Up Typesense InstantSearch Adapter

We’ll install and set up the typesense-instant search-adapter in this section.

In your terminal, where your newly created project is located:

  1. Run npm install --``save typesense-instantsearch-adapter
  2. Next, open your project in your code editor (I’ll be using VScode).

By default, InstantSearch.js uses Algolia configurations, we can configure it for Typesense by replacing the two lines of code below in your src/app.js file.

const { algoliasearch, instantsearch } = window;
const searchClient = algoliasearch('latency', '6be576ff63225e2a90f76');

With the following lines of code:

import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
import Typesense from 'typesense'

const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: process.env.TYPESENSE_SEARCH_ONLY_API,
    nodes: [
      {
        host: process.env.TYPESENSE_HOST,
        port: "443",
        protocol: "https"
      }
    ]
  },

  additionalSearchParameters: {
    queryBy: "title,category"
  }
});

const searchClient = typesenseInstantsearchAdapter.searchClient;

let client = new Typesense.Client({
  nodes: [
    {
      host: process.env.TYPESENSE_HOST, 
      port: '443',
      protocol: 'https', 
    },
  ],
  apiKey: process.env.TYPESENSE_ADMIN_API,
  connectionTimeoutSeconds: 2,
});

//...

From the above lines of code, we’re creating a connection with the Typesense adapter using our search-only API key, while we also create a new Typesense client with our admin API key since we’ll be needing write access to the cluster.

Instead of exposing sensitive credentials directly in our code, we’ll add them to our application as environmental variables.

To do this, create a new .env file and add the following lines of code:

TYPESENSE_HOST=<your-typesense-Nodes-url>
TYPESENSE_SEARCH_ONLY_API=<your-typesense-search-only-api-key>
TYPESENSE_ADMIN_API=<your-typesense-admin-api-key>

Replace the placeholders with your cluster credentials, and make sure not to push your .env to a version control platform like GitHub.

Run npm run start to start your application server, then visit the port displayed in your terminal on your browser.

starting your react app server

Your browser should show a custom header and a search bar, along with an error in the console.

instant search js default page

To resolve this error, go to your src/app.js file:

Locate and comment out the lines of code below:

instantsearch.widgets.configure({
  facets: ['*'],
  maxValuesPerFacet: 20,
}),

instantsearch.widgets.dynamicWidgets({
  container: '#dynamic-widgets',
  fallbackWidget({ container, attribute }) {
    return instantsearch.widgets.refinementList({
      container,
      attribute,
    });
   },
  widgets: [],
}),

Now, our page should look like below:

configuring the instant search widget

If you wonder where the search bar and pagination are coming from, in your app.js file you will see a search.addWidgets declaration. This is where we’ll add our custom widgets soon, and by default, InstantSearch.js boilerplate has added the searchBox , hits, and pagination widget for us.

Your search.addWidgets should now look like this: src/app.js

//...
search.addWidgets([
  instantsearch.widgets.searchBox({
    container: '#searchbox',
  }),
  instantsearch.widgets.hits({
    container: '#hits',
  }),
  // instantsearch.widgets.configure({
  //   facets: ['*'],
  //   maxValuesPerFacet: 20,
  // }),
  // instantsearch.widgets.dynamicWidgets({
  //   container: '#dynamic-widgets',
  //   fallbackWidget({ container, attribute }) {
  //     return instantsearch.widgets.refinementList({
  //       container,
  //       attribute,
  //     });
  //   },
  //   widgets: [],
  // }),
  instantsearch.widgets.pagination({
    container: '#pagination',
  }),
]);
//...

The HTML id identifier (#) that each widget refers to as container can be found in your index.html file. index.html

<!-- ... -->
<div class="container">
  <div class="search-panel">
    <div class="search-panel__filters">
      <div id="dynamic-widgets"></div>
    </div>
    <div class="search-panel__results">
      <div id="searchbox"></div>
      <div id="hits"></div>
    </div>
  </div>
  <div id="pagination"></div>
</div>
<!-- ... -->

Adding Documents to Typesense

This section will walk you through how to add documents to your Typesense cluster. If collections are table in a relational database, then documents should be seen as individual entries in a table.

You can add documents to Typesense in a few ways: one is through the Typesense dashboard, or through the Typesense client import method in your code. In this tutorial, we'll be using the latter.

We’ll be making use of Axios to make a GET request to fakestoreapi.com/products.

Install Axios using npm install axios.

Copy and paste the importProduct function below into your app.js file after installing Axios.

// add to top
import axios from 'axios';
// ...
const importProducts = async () => {
  try {

    const response = await axios.get('https://fakestoreapi.com/products');

    const convertIdToString = (arrayOfProducts) => {
      return arrayOfProducts.map(product => {
        product.id = product.id.toString();
        return product;
      });
    };

    // Typesense document's ID must be a typeof string
    const products = convertIdToString(response.data);

    // Import the products as document to Typesense
    client
      .collections('products')
      .documents()
      .import(products, { action: 'create' });

  } catch (error) {
    throw new Error(error);
  }
}

// Call the function to import the products to Typesense
importProducts();

We fetch our products from our API and then convert the ID of each product to a string in the above snippet; please note that the Typesense document ID must be a type of string or else the import will fail. Finally, as soon as the app.js file loads, we’re calling the function to import the products into Typesense.

Below is the resulting output of the above function.

Adding Hit Widget Template

In this section, we’ll add a template to customize our product card using the hit widget template. The hit widget is a component that displays the result of a search using HTML.

Update the hit widget (instantsearch.widgets.hits) with the following lines of code. app.js

instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      item({ image, title, price, rating }) {
        return `
          <div class="product-card">
            <div>
             <img src="${image}" align="left" alt="" />
            </div>
            <span class="hit-rate">
              ${rating.rate}
            </span>
            <h2 class="hit-title">
             ${title}
            </h2>
            <p class="hit-price">
              $ ${price}
            </p>
         </div>
      `;
    },
  },
}),

From the code above, we’re declaring our custom template and using the ES6 destructuring to pull out our product data.

To style the UI, paste the CSS code below into your index.css file:

img {
  width: 100%;
  height: 186px;
  margin-bottom: 1rem;
}
.hit-rate {
  font-size: 0.75rem;
  background: #fedd4e;
  border-radius: 50%;
  padding: 5px;
  text-align: right;
  color: #3a4570;
}
.hit-title {
  font-size: 0.875rem;
  margin-top: 1rem;
  font-weight: 600;
  height: 100px;
}
.hit-price {
  font-size: 14px;
  margin-top: 1rem;
}

Save your file and refresh your browser; your product card should now look like this 👇

integrating instantsearch with typesense

And not only that, you can start searching through the products.

instant search functionality working

Adding Placeholder to InstantSearch.js Search Widget

This section shows how to add a placeholder to InstantSearch.js search box. To achieve this we’ll set the placeholder property of the search widget like below:

instantsearch.widgets.searchBox({
  container: '#searchbox',
  // new line of code
  placeholder: 'Search for a product',
}),

The resulting output:

Setting Hit Per Page

The number of search results displayed to the user is referred to as hits. By default, the first ten search results are shown. We can change this by adding the following configuration to our InstantSearch widget:

instantsearch.widgets.configure({
  hitsPerPage: 8,
}),

This will display 8 products per page:

instant search displaying product per specified count

You can adjust the number to see it in action.

Adding InstantSearch SortBy Widget

In this section, we’ll add the sort-by functionality to our e-commerce website. The sortby Widget will allow the user to sort the products using a drop down menu depending on high or low pricing.

instantsearch.widgets.sortBy({
  container: '#sort-by',
  items: [
    { label: 'Default Sort', value: 'products' },
    { label: 'Price: Low to High', value: 'products/sort/price:asc' },
    { label: 'Price: High to Low', value: 'products/sort/price:desc' },
  ],
}),

Create the sort-by container in your index.html file like below:

<div class="search-panel__results">
  <div id="searchbox"></div>
  <div id="sort-by"></div> <!-- new line -->
  <div id="hits"></div>
</div>

Now we can sort our product by price.

sorting products by price using instantsearch and typesense

Adding InstantSearch Hit Per Page Dropdown Widget

In this section, we’ll add the hit per page widget functionality, similar to the hit per page that we’ve configured earlier, but this widget allows us to determine how many hits should be displayed per page using a dropdown.

Add the following lines of code to your app.js file to add the hit per page widget for 8 (default) and 12 hits per page options.

instantsearch.widgets.hitsPerPage({
   container: '#hits-per-page',
   items: [
      { label: '8 per page', value: 8, default: true },
      { label: '12 per page', value: 12 },
    ],
}),

Create the hit-per-page container in your index.html file like below:

<div class="search-panel__results">
  <div id="searchbox"></div>
  <div id="sort-by"></div>
  <div id="hits-per-page"></div> <!-- new line -->
  <div id="hits"></div>
</div>

Now we can determine how many products should be displayed per page from the dropdown.

demonstrating per page display by instantsearch and typesense

Adding InstantSearch Stats Widget

In this section, we’ll add the stats widget to notify the user of how many products are found in their search result.

Add the following lines of code to your app.js to add the stats widget.

instantsearch.widgets.stats({
  container: '#stats',
  templates: {
    text: `
    {{#hasNoResults}}No products{{/hasNoResults}}
    {{#hasOneResult}}1 product{{/hasOneResult}}
    {{#hasManyResults}}{{#helpers.formatNumber}}
    {{nbHits}}{{/helpers.formatNumber}} 
    products{{/hasManyResults}} found in {{processingTimeMS}}ms
  `,
  },
}),

Create the stats container in your index.html file like below:

<div class="search-panel__results">
  <div id="searchbox"></div>
  <div id="sort-by"></div>
  <div id="hits-per-page"></div>
  <div id="stats"></div> <!-- new line -->
  <div id="hits"></div>
</div>

Now we can see how many products are returned from our search and how long it takes in milliseconds.

showing how long loading the product takes in instantsearch js

Styling our Widgets

In this section, we’ll apply some vanilla CSS styles to our widgets so that we'll have a nice layout.

Update your search-panel__results container with the code below: index.html

<div class="search-panel__results">
  <div id="searchbox"></div>
  <section class="product-stat">
    <div id="sort-by"></div>
    <section class="right-product-stat">
      <div id="hits-per-page"></div>
      <div id="stats"></div>
    </section>
  </section>
  <div id="hits"></div>
</div>

Next, add the CSS styles below to your index.css file.

#searchbox {
  margin-top: 20px;
}
.product-stat {
  display: flex;
  justify-content: space-between;
  margin: 1rem 0;
}
.right-product-stat {
  justify-content: flex-end;
  display: flex;
  align-items: baseline;
  gap: 1rem;
}

Our widgets should look much better as shown below:

styling instant search product statistics

Adding Category Refinement List Widget

In this section, we’ll add the refinementList widget for our product category. The category refinement list widget will display the available product categories with a counter showing the numbers of products in each category.

Add the following lines of code to your app.js to add the category refinementList widget.

instantsearch.widgets.refinementList({
  container: '#category-refinement-list',
  attribute: 'category',
}),

Create the #category-refinement-list container in your index.html file like below:

<div class="search-panel__filters">
  <h3>Category</h3>
  <div id="category-refinement-list"></div>
</div>

Our category refinement widget will be located at the left side as shown below:

implementing category refinement widget

In the next section, we’ll add the price refinement list widget.

Adding Price Refinement List Widget

To add the price or another refinement list widget, add following lines of code to your app.js

instantsearch.widgets.refinementList({
  container: '#price-refinement-list',
  attribute: 'price',
}),

Create the #price-refinement-list container in your index.html file like below:

<div class="search-panel__filters">
  <h3>Category</h3>
  <div id="category-refinement-list"></div>

<!-- new lines -->
  <h3>Price ($)</h3>
  <div id="price-refinement-list"></div>
</div>

Now, you can see the refinement list widget in action 🎉

demonstrating the refinement list widget

Adding Range Slider Widget

In this section, we’ll add the price range slider functionality. This will allow the user to filter products by price using a slider.

Add the following code to your app.js

instantsearch.widgets.rangeSlider({
  container: '#price-range-slider',
  attribute: 'price',
}),

Next, add the #price-range-slider container to your index.html file as shown below:

<div class="search-panel__filters">
  <h3>Category</h3>
  <div id="category-refinement-list"></div>

  <h3>Price ($)</h3>
  <div id="price-refinement-list"></div>

  <!-- new lines -->
  <h3>Price Slider ($)</h3>
  <div id="price-range-slider"></div>
</div>

Below is the resulting output:

adding search panel filters

Update your index.css with the CSS code below:

.ais-RefinementList-count {
  background-color: #fedd4e;
}
#price-refinement-list,
#category-refinement-list {
  text-transform: capitalize;
}
#price-range-slider {
  padding-right: 3rem;
}
.ais-RangeSlider .rheostat-progress,
.ais-RangeSlider .rheostat-marker {
  background-color: #fedd4e;
}

The CSS above will produce the result below:

styling the range slider in instantsearch js

Congratulations!!! 🎉

Let’s see our e-commerce website functionalities in action:

Bringing it together

Sorting, searching, and filtering functionalities are important features for websites that deliver a large amount of data to their users. These features can also be challenging for developers to create from scratch.

Typesense along with InstantSearch makes the implementation of these functionalities easier for your JavaScript, React, Vue, or Angular projects.

In this article, you saw how to use InstantSearch.js with Typesense to create rapid search and InstantSearch Analytics Widgets.

  • You can find the complete code here

Wow, what a journey, I am glad you made it to the end of this article, feel free to ask me any question in the comment section and if you enjoyed and learned something new from this article, I will like to connect with you.

Let's connect on👇



See you in the next article. Bye Bye 🙋‍♂️

unclebigbay blog

If you found my content helpful and want to support my blog, you can also buy me a coffee below, my blog lives on coffee 🙏.