Alright, alright, alright. I heard you like React, and Airbnb, but what is this Elasticsearch thing I'm talking about. Don't worry. This blog post will give you a kickstart in understanding the basics of Elasticsearch - What is it? Why should you care about it? and, how can you use it with React to build powerful apps painlessly.

What is Elasticsearch and why should you care about it?

Elasticsearch is a super fast open-source full-text search engine. It allows you to store, search, and analyze big volumes of data quickly (we are talking milliseconds here). It is generally used as the underlying engine/technology that powers applications that have complex search features and requirements. You can read more about it here.

With Elasticsearch, you can build build a fast search utilizing its powerful query DSL. However, setting up elasticsearch correctly requires a lot of work. For instance, the data 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.

We, at Appbase, have built some open-source tools to help you do all these things with the matter of some clicks.

  • Tool to add data into Elasticsearch - Importer
  • Tool to view Elasticsearch data like an excel sheet - Data Browser
  • Tool to generate relevant Elasticsearch queries easily - Query Builder

In this blog post, with the help of some of these tooling, we will utilize the strengths of Elasticsearch with React to build powerful apps.

How to use Elasticsearch with React?

We will be using - ReactiveSearch, an open-source React UI components library for Elasticsearch that I am a contributor to. It offers a range of highly customizable rich UI components that can connect with any Elasticsearch server and provide you an appropriate default search query for all generic use-cases (like Ecommerce, Yelp, Meetups, etc) bundled into these components.

Wait, why do I need ReactiveSearch now?

ReactiveSearch simplifies the entire process of connecting to an elasticsearch index, making queries, fetching and rendering results in sleek UI, not just that, it also lets you make your components talk to each other, i.e. if Component-A gets updated, Component-B gets to know and it can update itself without needing any manual interaction.

This whole component-to-component subscription comes in handy when you have dynamic filters present on your screen, such as in case of E-commerce apps where Selecting a category of Appliances, also changes the sub-categories available, their prices and what not.

ReactiveSearch helps you create significantly smarter apps easily and in a declarative fashion.

Even if you’ve never used Elasticsearch before you should be able to follow along with this blog post and build your very own Elasticsearch powered search UI using ReactiveSearch.

Here is an example that generates a searchbox UI with some suggestions as you type.

<DataSearch
    componentId="search"
    dataField="name"
    placeholder="Search housings..."
    iconPosition="left"
/>

Things we will need

In order to build an Airbnb-like search application, we will need a set of things before we get started with writing actual code:

  • Dataset for Airbnb housing properties - We found an amazing dataset for Airbnb Seattle at https://www.kaggle.com/airbnb/seattle which we will be using for our app in this tutorial.

  • Elasticsearch hosting - You can set up and install an Elasticsearch server by following the official installation guide, or you can create a free account at appbase.io which provides Elasticsearch hosting as a service and is easy to use. For simplicity, we will be using appbase.io service to get started with.

I've already created an appbase app with airbnb data-set. You can check out the cleaned up dataset over here in the data browser tool:

Airbnb Dataset Browser

The credentials of the above app are:

app="housing"
credentials="0aL1X5Vts:1ee67be1-9195-4f4b-bd4f-a91cd1b5e4b5"
type="listing"

You can clone this dataset and generate credentials for your very own app by clicking on the Clone this app button on the bottom left.

Setup

If you have tried out the Create React App before, you will feel right at home as we build this app.

Create React App

Initialize the CRA setup. We will use yarn as the package manager, you can also use npm instead.

yarn global add create-react-app
create-react-app airbeds
cd airbeds
yarn start

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

At this point, you should have a directory structure similar to this:

airbeds
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── yarn.lock
├── public
│   └── favicon.ico
│   └── index.html
│   └── manifest.json
└── src
    └── App.css
    └── App.js
    └── App.test.js
    └── index.css
    └── index.js
    └── logo.svg
    └── registerServiceWorker.js

Install reactivesearch

Next, we will install ReactiveSearch.

yarn add @appbaseio/reactivesearch

Next step is to connect with our elasticsearch index at appbase (or your own Elasticsearch server if you are not using appbase).

Adding the first ReactiveSearch component: ReactiveBase

All the ReactiveSearch components are wrapped inside a container component - ReactiveBase which glues the Elasticsearch index and the ReactiveSearch components together. We’ll use this in /src/App.js:

import React, { Component } from 'react';
import { ReactiveBase } from '@appbaseio/reactivesearch';

class App extends Component {
  render() {
    return (
      <section className="container">
        <ReactiveBase
          app="housing"
          credentials="0aL1X5Vts:1ee67be1-9195-4f4b-bd4f-a91cd1b5e4b5"
          type="listing"
        >
          Hello from ReactiveSearch!
        </ReactiveBase>
      </section>
    );
  }
}
export default App;

Notice that we are using the app credentials from the previous step. You may use the ones I’ve provided here as is, or if you cloned this dataset in your own app earlier, you can get them from the app’s credentials page - copy the Read-only API key.

If you’re already familiar with Elasticsearch, and have decided to use an Elastisearch server of your own, you will need to pass a url prop referring to your own Elasticsearch cluster URL.

Now let’s start the server with yarn start.

Adding UI Components

Components we will need to begin with:

  • First in the list is a calendar UI which allows a user to select a range of dates for accommodation. The component looks like:
import { DateRange } from '@appbaseio/reactivesearch';

<DateRange
    dataField="date_from"
    componentId="DateRangeSensor"
    title="When"
    numberOfMonths={1}
    queryFormat="basic_date" // yyyyMMdd
/>

where,

componentId is a mandatory prop which requires specifying a unique string which indentifies this particular component. It 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).

Since dates can be written in many different formats, like milliseconds since epoch, or yyyyMMdd, or YYYY-MM-DD or one with an associated timestamp etc, we need to tell the DateRange component about the indexed date field’s data format using the queryFormat prop. You can read more about the DateRange component in its document reference over here.

  • Then comes the guest selection UI which allows a user to filter properties by the number of guests that can be accommodated.

Here, we will use NumberBox component:

import { NumberBox } from '@appbaseio/reactivesearch';

<NumberBox
    componentId="GuestSensor"
    dataField="accommodates"
    title="Guests"
    defaultSelected={2}
    data={{
        start: 1,
        end: 16
    }}
/>

where,

the data prop accepts an object with start and end values for the minimum and maximum guests that will be accommodated and the defaultSelected prop initializes with the component’s UI with a value from within the data range.

  • Next up is the price filter which allows a user to filter accommodations based on their budgets:
import { RangeSlider } from '@appbaseio/reactivesearch';

<RangeSlider
    componentId="PriceSensor"
    dataField="price"
    title="Price Range"
    range={{
        start: 10,
        end: 250
    }}
    rangeLabels={{
        start: "$10",
        end: "$250"
    }}
    defaultSelected={{
        start: 10,
        end: 50
    }}
    stepValue={10}
    react={{
        and: ["DateRangeSensor"]
    }}
/>

Here, we created a RangeSlider, which has a configurable range prop, that controls the bounds of the slider, and also allow us to add labels and pre-select a range using rangeLabels and defaultSelected props. You can read about the RangeSlider component over here.

This time around, we also use a new react prop, which updates the prices dynamically based on the selected date range and number of guests. Every time there is a change in the dates or the number of guests, the price range histogram changes. This is a key feature of ReactiveSearch that lets you create dynamically updating UI components. You can read more about it here.

  • Lastly, we will need those fancy Airbnb cards to show the search results:
import { ResultCard } from '@appbaseio/reactivesearch';

<ResultCard
    componentId="SearchResult"
    dataField="name"
    from={0}
    size={10}
    onData={this.onData}
    pagination={true}
    react={{
        and: ["GuestSensor", "PriceSensor", "DateRangeSensor"]
    }}
/>

Besides all the straight-forward props, ResultCard takes in a special onData prop which is a callback function that returns the mappings between the elements of the ResultCard and the fields in the database. For our case, the onData method would look something like this:

onData(data) {
    return {
        image: data.image,
        title: data.name,
        description: (
            <div>
                <div className="price">${data.price}</div>
                <p className="info">{data.room_type} · {data.accommodates} guests</p>
            </div>
        ),
        url: data.listing_url,
    };
}

Here, we are returning an object that maps the image, title, desc and url fields of the card layout to the specific fields in the the database or a valid React element expressed in JSX.

Combining the Elements

Lets put these these components together in the src/App.js file. While doing this, I will also add placeholder classes in the className and innerClass props that we will inject via a stylesheet in the next section.

import React from 'react';
import { ReactiveBase, NumberBox, DateRange, RangeSlider, ResultCard } from '@appbaseio/reactivesearch';


export default () => (
    <div className="container">
        <ReactiveBase
            app="housing"
            credentials="0aL1X5Vts:1ee67be1-9195-4f4b-bd4f-a91cd1b5e4b5"
            type="listing"
            theme={{
                primaryColor: '#FF3A4E',
            }}
        >
            <nav className="nav">
                <div className="title">airbeds</div>
            </nav>
            <div className="left-col">
                <DateRange
                    dataField="date_from"
                    componentId="DateRangeSensor"
                    title="When"
                    numberOfMonths={2}
                    queryFormat="basic_date"
                    initialMonth={new Date('04-01-2017')}
                />

                <NumberBox
                    componentId="GuestSensor"
                    dataField="accommodates"
                    title="Guests"
                    defaultSelected={2}
                    labelPosition="right"
                    data={{
                        start: 1,
                        end: 16,
                    }}
                />

                <RangeSlider
                    componentId="PriceSensor"
                    dataField="price"
                    title="Price Range"
                    range={{
                        start: 10,
                        end: 250,
                    }}
                    rangeLabels={{
                        start: '$10',
                        end: '$250',
                    }}
                    defaultSelected={{
                        start: 10,
                        end: 50,
                    }}
                    stepValue={10}
                    interval={20}
                    react={{
                        and: ['DateRangeSensor'],
                    }}
                />
            </div>

            <ResultCard
                className="right-col"
                componentId="SearchResult"
                dataField="name"
                size={12}
                onData={data => ({
                    image: data.image,
                    title: data.name,
                    description: (
                        <div>
                            <div className="price">${data.price}</div>
                            <p className="info">{data.room_type} · {data.accommodates} guests</p>
                        </div>
                    ),
                    url: data.listing_url,
                })}
                pagination
                react={{
                    and: ['GuestSensor', 'PriceSensor', 'DateRangeSensor', 'search'],
                }}
                innerClass={{
                    resultStats: 'result-stats',
                    list: 'list',
                    listItem: 'list-item',
                    image: 'image',
                }}
            />
        </ReactiveBase>
    </div>
);

You should see a fully functional UI that works. Slide through the prices and you will see the results change in the card layout. The only thing missing at this point is the layout arrangement and the styles. We will add these in the next step.

Adding Styles and Layout

ReactiveSearch provides us with scoped styled components, while leaving the choice of layout to the user.

To keep things simple, we will use the following stylesheet and get things going:

.container {
  display: flex;
  padding-top: 52px;
}
.container .nav {
  width: 100%;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #FF3A4E;
  color: #fff;
  height: 52px;
  padding: 0 25px;
}
.container .left-col {
  width: 320px;
  height: 100%;
  padding: 15px 20px;
  position: fixed;
  left: 0;
  right: 0;
  border-right: 1px solid #f0f0f0;
}
.container .left-col > div {
  margin: 40px 0;
}
@media all and (max-width: 768px) {
  .container .left-col {
    position: static;
    width: 100%;
    height: auto;
    border-right: 0;
    border-bottom: 1px solid #f0f0f0;
  }
}
.container .right-col {
  width: calc(100% - 320px);
  position: relative;
  left: 320px;
  padding: 25px 30px;
  background-color: #fbfbfb;
}
.container .right-col .list {
  margin-bottom: 30px;
}
.container .right-col .list-item {
  max-width: none;
  min-width: 0;
  width: calc(30% - 16px);
  height: auto;
  background-color: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  position: relative;
  padding: 0;
}
.container .right-col .list-item h2 {
  padding-bottom: 4px;
}
.container .right-col .list-item .image {
  background-size: cover;
}
.container .right-col .list-item .price {
  width: 70px;
  height: 44px;
  background-color: #424242;
  position: absolute;
  top: 160px;
  left: 0;
  color: #fafafa;
  font-size: 18px;
  display: flex;
  justify-content: center;
  align-items: center;
  letter-spacing: 0.03rem;
}
.container .right-col .list-item .info {
  color: #555;
  font-size: 14px;
  margin-bottom: 4px;
}
.container .right-col .result-stats {
  text-align: right;
  color: #666;
  font-size: 15px;
}
@media all and (min-width: 1441px) {
  .container .right-col .list-item {
    width: calc(25% - 16px);
  }
}
@media all and (max-width: 1024px) {
  .container .right-col .list-item {
    width: calc(50% - 16px);
  }
}
@media all and (max-width: 768px) {
  .container .right-col {
    width: 100%;
    position: static;
    padding: 25px 15px;
  }
}
@media all and (max-width: 480px) {
  .container .right-col .list-item {
    width: calc(100% - 16px);
    margin-bottom: 20px;
  }
}

Majority of the styling here is about setting the layout. Add the above css styles to App.css and import it in your App.js with a import './App.css' statement along our other imports and we are done!

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

git clone git@github.com:appbaseio-apps/airbeds.git
cd airbeds && git checkout basic-app
yarn && yarn start
# open http://localhost:3000

We went from a boilerplate with CRA to creating an Airbnb-like UI, hopefully well within 60 minutes.

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:

  • Add a searchbox or more filters (ReactiveSearch offers 20+ components),
  • Add sort options to allow different ordering of the results,
  • Add an OAuth login flow and only allow signed-in users to see this UI view.

Tap on the above image to go to ReactiveSearch Docs to explore further.

Go star the Reactivesearch Github project so you can find it when you need to build that awesome search.