How to Build an E-commerce Search UI with React and Elasticsearch

Siddharth Kothari
codeburst
Published in
7 min readJan 4, 2018

--

Image: A flavor of the booksearch app with more filters and sort options

Follow along this updated tutorial post instead over here that uses v3 of ReactiveSearch and React, updated on April 29, 2022

We will build an e-commerce book search UI using React and Elasticsearch — in less than 60 minutes!

Building a search UI requires two key components:

  1. A powerful search backend — Elasticsearch here fits the bill, being the #1 search engine.
  2. A well designed UI — React is a great choice for undertaking this endeavor in.

However, wielding these two powerful tools effectively requires a lot of work. For instance, the mapping, analyzers and tokenizers need to be set correctly or you may not receive accurate search results back. Besides, the more filters that get applied along with the search query, the more complex the resulting search query becomes. And designing data-driven UI views where state is synced with the backend is no trivial feat.

This is where ReactiveSearch comes to aid, an open-source React UI components library for Elasticsearch that I am a contributor to. It offers 25+ rich UI components that provide a powerful scaffolding for building data-driven search UIs.

In this post, I will show how to use ReactiveSearch for building a booksearch UI in less than an hour.

Before we Get Started

We will need a good dataset. We will borrow one about Goodbooks from https://github.com/zygmuntz/goodbooks-10k. You can check out the cleaned up version that is already indexed into Elasticsearch over here.

Image: Tap on the image to view and/or “clone” this dataset.

Next, we will need an Elasticsearch index to host our backend data. In this post, we will use appbase.io for doing this but you can also run your own Elasticsearch cluster and create an index.

Image: Showing an app (aka index) creation process within appbase.io

If you opt for cloning the above dataset, you would have already created an appbase.io app.

Getting Started with Create React App

Now that we have the dataset and indexing figured out, we are ready to get started with building the app.

We will initialize a boilerplate with the CRA setup.

npm install -g create-react-app  # install CRA if you don't have it.
create-react-app booksearch # initialize the boilerplate.
cd booksearch

Let’s test the default CRA app by running npm start.

Image: Output at http://localhost:3000 after setting up CRA.

One of the great benefits of using CRA is that it works without requiring to set up a build configuration.

Add ReactiveSearch

Next, install @appbaseio/reactivesearch via npm.

npm install --save @appbaseio/reactivesearch@2.16.1

Note: ReactiveSearch v3 is now available and this post isn’t compatible with that. Installing v2.16.1 (or any other v2.x) ensures that you can follow this post as is.

All the ReactiveSearch components are wrapped inside a container component — ReactiveBase which glues the Elasticsearch index and the ReactiveSearch components together. Edit src/App.js file.

import React, { Component } from 'react';
import { ReactiveBase } from '@appbaseio/reactivesearch';
class App extends Component {
render() {
return (
<ReactiveBase
app="good-books-ds"
credentials="nY6NNTZZ6:27b76b9f-18ea-456c-bc5e-3a5263ebc63d"
>
Hello from Reactive Search!
</ReactiveBase>
);
}
}
export default App;

The app and credentials refer to the index name and any credentials you may have set up to authorize access. ReactiveBase also supports a url prop which takes in the Elasticsearch cluster URL.

Note: You should either use read-only credentials or have an authorization middleware that ReactiveBase connects to for ensuring secure access in a production build.

Let’s start the server with npm start.

Image: Output at http://localhost:3000 after adding reactivesearch

Adding UI Components

Components we will need to begin with:

  1. A books data search box,
  2. A books ratings filter,
  3. A display of books result.

DataSearch

Image: DataSearch component UI using the adjacent snippet.
<DataSearch
componentId="mainSearch"
dataField={["original_title", "authors"]}
queryFormat="and"
/>

componentId is a mandatory prop which requires specifying a unique string that is used internally by ReactiveSearch lib, as well as by some of the other user facing props.

dataField prop tells DataSearch which fields to query on. It can take either a string (single field) or an Array of strings (multiple fields).

queryFormat prop’s and value tells DataSearch to only return those results where the search query is present across all the dataField values.

We will eventually perform more customizations in the following steps. You can find the full reference of DataSearch component over here.

SingleRange

Image: SingleRange component UI from the snippet below.
<SingleRange
componentId="ratingsFilter"
dataField="average_rating_rounded"
title="Book Ratings"
data={[
{ start: 4, end: 5, label: "★★★★ & up" },
{ start: 3, end: 5, label: "★★★ & up" },
{ start: 2, end: 5, label: "★★ & up" },
{ start: 1, end: 5, label: "★ & up" }
]}
/>

data prop here allows us to define the range [start, end] options with a label value. When a user selects one of the range options, a range query is applied on the dataField with the selected range endpoints.

You can find the full reference of SingleRange component over here.

ResultCard

Image: ResultCard component UI from the snippet below.
<ResultCard
componentId="results"
dataField="original_title"
react={{
and: ["mainSearch", "ratingsFilter"]
}}
onData={(res) => (
{
"image": res.image,
"title": res.title,
"description": res.average_rating + " ★ "
}
)}
/>

As the name suggests, the ResultCard component displays the results in a card layout.

react prop tells the ResultCard component to construct the query based on the individual queries used within the DataSearch and SingleRange components we defined above (referenced by their componentId value).

onData prop is a function that receives one hit object as an argument and returns the card markup.

Full reference of what ResultCard component allows can be found over here.

Combining the Elements

Lets put these three components together in the src/App.js file.

import React, { Component } from 'react';
import {
ReactiveBase,
DataSearch,
SingleRange,
ResultCard
} from '@appbaseio/reactivesearch';
class App extends Component {
render() {
return (
<ReactiveBase
app="good-books-ds"
credentials="nY6NNTZZ6:27b76b9f-18ea-456c-bc5e-3a5263ebc63d"
>
<DataSearch
componentId="mainSearch"
dataField={["original_title", "original_title.search", "authors", "authors.search"]}
queryFormat="and"
iconPosition="left"
/>
<SingleRange
componentId="ratingsFilter"
dataField="average_rating_rounded"
title="Book Ratings"
data={[
{ start: 4, end: 5, label: "★★★★ & up" },
{ start: 3, end: 5, label: "★★★ & up" },
{ start: 2, end: 5, label: "★★ & up" },
{ start: 1, end: 5, label: "★ & up" },
]}
react={{
and: "mainSearch"
}}
/>
<ResultCard
componentId="results"
dataField="original_title"
react={{
"and": ["mainSearch", "ratingsFilter"]
}}
onData={(res)=>({
"image": res.image,
"title": res.original_title,
"description": res.average_rating + " ★ "
})}
/>
</ReactiveBase>
);
}
}
export default App;

Let’s start the server to see how the UI looks with npm start .

Image: Our app UI after adding three components :-)

You should see a fully functional UI that works. Type a search query to see the results reflect in the cards UI. The only thing missing at this point is the layout arrangement and the styles. We will add these in the next step.

Note: We didn’t break a sweat so far and have been able to work around Elasticsearch’s Query DSL complexity, didn’t need to write complex rendering logic nor manage state bindings to our UI view.

Adding Styles and Layout

ReactiveSearch provides us with scoped styled components, while leaving the choice of layout to the user. In this post, we will use Flex to arrange the components but we could’ve also used Materialize, Bootstrap or a CSS Grid. If you are new to Flex, I recommend reading this article.

We will add a navbar and give our app a nice logo, along with adding some layout and styles to each of the components. To do this, we will take benefit of the className and innerClass props.

Here’s our final src/App.js with the changes.

We also import src/App.css this time around to include our user-defined styles.

Majority of the stylesheet here is about setting the layout.

After changing these two files and running npm start , you should see a UI like this.

Image: Our App UI with layout and styles applied.

Looks much better, right!

In case you are missing a step, you can get the code so far by following these:

git clone git@github.com:appbaseio-apps/booksearch.git
cd booksearch && git checkout basic-app
npm install && npm start
# go to http://localhost:3000

What can we do next?

We have just scratched the surface here of what’s possible to build. Some ideas that you can build upon here are:

  1. Add more filters (ReactiveSearch offers 20+ components),
  2. Add sort options to allow different ordering of the results,
  3. Add an OAuth login flow and only allow signed-in users to see this UI view.
Image: All the UI components that ReactiveSearch offers

I have created a flavor with some additional filters that looks like this:

Image: A flavor of the booksearch app with more filters and sort options

I have added two new components here: RangeSlider and MultiList, and tweaked some props in the DataSearch and ResultCard components.

You can also see the code for the same at https://github.com/appbaseio-apps/booksearch/tree/master.

Conclusion

We went from a boilerplate with CRA to creating a data-driven booksearch UI, hopefully well within 60 minutes.

Here are some relevant links to help with further exploring:

  1. Code Repository — https://github.com/appbaseio-apps/booksearch,
  2. ReactiveSearch Repo — https://github.com/appbaseio/reactivesearch,
  3. Components playground — https://opensource.appbase.io/playground/,
  4. Documentation — https://opensource.appbase.io/reactive-manual/.

--

--