๐ŸŽ“ Tutorial: Build Search UI with OpenSearch and ReactiveSearch

๐ŸŽ“ Tutorial: Build Search UI with OpenSearch and ReactiveSearch

A 101 tutorial showing how to set up OpenSearch and ReactiveSearch, ingest data and build a follow-along search UI with React

ยท

9 min read

In this post, I will walk through a follow-along tutorial for building search UIs powered by OpenSearch and ReactiveSearch. I will show how to set up OpenSearch and ReactiveSearch API with Docker, and then build a search UI using ReactiveSearch's UI components. You should be able to follow the entire setup end-to-end in under an hour! ๐Ÿ’ช

Let's start with an introduction: OpenSearch is an Apache 2.0 licensed fork of Elasticsearch maintained by Amazon and contributors. ReactiveSearch is one of the most popular UI kits for building search UIs with Elasticsearch, available for React, Vue, React Native, Vanilla JS (headless support) and Flutter. More than 1,000 projects use ReactiveSearch UI kits today to power their search experiences.

Flow

This diagram shows the data flow of search experiences powered with ReactiveSearch

ReactiveSearch and Searchbox UI components work out of the box with OpenSearch. They make use of the ReactiveSearch API server: a declarative API purpose built for powering search UIs powered by Elasticsearch or OpenSearch.

Note: You can also use ReactiveSearch API to build search UIs powered by MongoDB Atlas Search. Support for Solr and more search engines is coming soon!

Besides offering a declarative DSL for specifying search queries and translating these to Elasticsearch / OpenSearch DSL at runtime, ReactiveSearch API server also offers out-of-the-box search analytics and insights and a search relevance control plane.

Setting up the Infra

We will start out by setting up OpenSearch and ReactiveSearch API server using this docker-compose file. Both the services used here are open-source and free and do not require any sign up / registration for using them.

version: '3'
services:
  opensearch:
    image: 'opensearchproject/opensearch:2.5.0'
    container_name: opensearch
    environment:
      - plugins.security.disabled=true
      - discovery.type=single-node
      - http.port=9200
      - bootstrap.memory_lock=true
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
    ports:
      - '9200:9200'
      - '9600:9600'
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - './data:/usr/share/opensearch/data'
  reactivesearch-api:
    container_name: rs-api
    image: 'ghcr.io/appbaseio/reactivesearch-api:8.9.3'
    restart: always
    depends_on:
      - opensearch
    environment:
      - 'ES_CLUSTER_URL=http://opensearch:9200'
      - USERNAME=rs-admin-user
      - PASSWORD=rs-password
    volumes:
      - 'reactivesearch-data:/reactivesearch-data'
    ports:
      - '8000:8000'
  fluentbit:
    image: 'fluent/fluent-bit:1.9.10'
    user: root
    container_name: fluentbit
    command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit.conf
    depends_on:
      - reactivesearch-api
    restart: always
    volumes:
      - './fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf'
      - './parser.conf:/fluent-bit/etc/parser.conf'
      - 'reactivesearch-data:/reactivesearch-data'
volumes:
  reactivesearch-data:
    external: false
    name: reactivesearch-data

Description of the services used here:

  • opensearch: runs OpenSearch in a single-node mode. If running for production, use the official setup. Also if your OpenSearch service is running in a cloud environment, this service can be skipped.

  • reactivesearch-api: runs the 8.3.0 release version (released June 18, 2022) with minimal config. You can use the latest version available from here

  • fluentbit: is the log shipper used by reactivesearch-api to send request/search analytics logs to opensearch

Doing a docker-compose up -d with the above file should output the following:

should output the following

You can then run curl localhost:8000 -u rs-admin-user:rs-password to confirm that both opensearch and reactivesearch-api servers are up.

For using the commercial version of appbase.io which includes search analytics, insights and search relevance control plane, use this docker-compose file instead.

Ingesting some data

Now that the infra is set up, let's ingest a sample dataset. You can use OpenSearch bulk APIs, OpenSearch dashboard's UI or the ReactiveSearch control plane to do this.

Here, I will show how to use the ReactiveSearch control plane to do this.

Start by logging in using the URL and username, password we used above for the cURL request.

Screenshot 2022-06-22 at 7.03.42 PM.png

Once logged in, you can follow the interactive tutorial to import a movie store dataset of ~10,000 movie titles.

Screenshot 2022-06-22 at 7.05.33 PM.png

Start the Interactive Tutorial

Screenshot 2022-06-22 at 7.08.35 PM.png

Set the index name, I'm using movie-app

Screenshot 2022-06-22 at 7.09.22 PM.png

I've selected the movies dataset for this tutorial. Press the Next button followed by Import to get the data indexed.

Screenshot 2022-06-22 at 7.11.37 PM.png

At this point, the data is indexed. You can visualize it using the data browser or proceed to the next steps to see a sample search UI.

Screenshot 2022-06-22 at 7.12.13 PM.png

Once I set searchable fields, I can search the dataset using the search bar.

Screenshot 2022-06-22 at 7.13.20 PM.png

Next I can set aggregatable fields to add facets to the sample search UI. That's all! We saw how to ingest the dataset and build a sample search UI with nocode.

Building the Search UI with React

We're in the final leg of this tutorial where we will use the search index to build a search UI using React. You can also follow the quickstart guide over here.

The same steps also apply for Vue, you can follow the Vue quickstart guide over here.

Create React App Boilerplate

In this quickstart guide, we will create a movie-based search engine based on a dataset of 10,000 movie titles using ReactiveSearch.

This is how your final app will look like at the end of following this tutorial, in just 10 minutes ๐Ÿš€

We can either add ReactiveSearch to an existing app or create a boilerplate app with Create React App (CRA). For this quickstart guide, we will use the CRA.

create-react-app my-awesome-search && cd my-awesome-search

OR

Alternatively, you can go to Codesandbox.io and choose the React Template

React Template, Codesandbox

Step 1: Install ReactiveSearch

We will fetch and install reactivesearch module using yarn or npm.

yarn add @appbaseio/reactivesearch

or

npm install @appbaseio/reactivesearch

OR

Alternatively, you can directly add the @appbaseio/reactivesearch dependency to codesandbox.io.


Step 2: Adding the first component

Lets add our first ReactiveSearch component: ReactiveBase, it is a provider component that allows specifying the OpenSearch index to connect to.

We will update our src/App.js file to add ReactiveBase component.

import React from "react";
import { ReactiveBase, DataSearch } from "@appbaseio/reactivesearch";

function App() {
  return (
    <ReactiveBase
      url="https://appbase-demo-ansible-abxiydt-arc.searchbase.io"
      app="movie-app"
      credentials="1ea7529dafa1:1c018d05-fd5e-4cdb-af60-f20f0f50a96f"
      enableAppbase
    >
      {/* Our components will go over here */}
      Hello from ReactiveSearch ๐Ÿ‘‹
    </ReactiveBase>
  );
}

export default App;

This is how the app should look after running the yarn start command.


Step 3: Adding Search and Aggregation components

For this app, we will be using SearchBox, MultiList and SingleRange components for searching and filtering on the index. And ResultCard component for showing the search results.

Let's add them to the ReactiveBase component. But before we do that, we will look at the important props for each.

<SearchBox
    componentId="searchbox"
    dataField={[
      {
        field: "overview",
        weight: 3
      },
      {
        field: "overview.autosuggest",
        weight: 1
      },
      {
        field: "original_title",
        weight: 5
      },
      {
        field: "original_title.autosuggest",
        weight: 1
      }
    ]}
    placeholder="Search for movies"
/>

The SearchBox component creates a searchbox UI component that queries on the specified fields with weights as specified by dataField prop. That's all it takes to create a functional search component.

At this point, you should see the following:

MultiList

Next, we will add the MultiList component. As the name suggests, it creates a multiple selection aggregation (aka facet) to filter our search results.

 <MultiList
   componentId="genres"
   dataField="genres_data.keyword"
   title="Filter by Genres"
   aggregationSize={5}
 />

Aggregation components like MultiList fire a term-type query. You can think of a term query as an exact match query, unlike a search query which involves more nuances. The use of the .keyword suffix for the authors field informs the search engine that the query here is of an exact type.

The aggregationSize prop is used to specify the total aggregations (think buckets) that you want to be returned based on the dataField value.

Note: The dataField value in MultiList is of string type since aggregation is always performed on a single field. In contrast, you may want to search on multiple fields in different ways, so the DataSearch component uses an array of fields instead.

SingleRange

Next, we will add the SingleRange component for creating a release year-based filter for movies.

<SingleRange
    componentId="year"
    dataField="release_year"
    title="Filter by Release Year"
    data={[
      { start: 2010, end: 2022, label: "2010 onwards" },
      { start: 2000, end: 2010, label: "2000 to 2010" },
      { start: 1900, end: 2000, label: "< 2000" }
    ]}
    defaultValue="2010 onwards"
/>

The SingleRange operates on a numeric datatype field and fires a range query. The data prop of SingleRange allows specifying a [start, end] range and a label associated with it. Using defaultValue, we can preselect a particular option. In this case, we're preselecting all the movies that are released from 2010 onwards.

At this point, this is how our app should be looking:

We just added completely functional search and aggregation components!

Step 4: Adding Results Component

We just need a results component to display the books that we're searching for. We will use the ReactiveList component with the ResultCard preset.

<ReactiveList
    componentId="results"
    dataField="_score"
    size={6}
    pagination={true}
    react={{
      and: ["searchbox", "genres", "year"]
    }}
    render={({ data }) => (
      <ReactiveList.ResultCardsWrapper>
        {data.map((item) => (
          <ResultCard key={item._id}>
            <ResultCard.Image src={item.poster_path} />
            <ResultCard.Title
              dangerouslySetInnerHTML={{
                __html: item.original_title
              }}
            />
            <ResultCard.Description>{`${item.overview.slice(
              0,
              100
            )}...`}</ResultCard.Description>
          </ResultCard>
        ))}
      </ReactiveList.ResultCardsWrapper>
    )}
/>

The react prop here specifies that the result should depend on the queries for our searchbox, authors filter, and rating filter. It's pretty neat!

In the render method, we are using the ResultCard preset to iterate over each result (aka hit) and set the image, title, and description values of the card layout.

At this point, you should be seeing our entire app functionally (minus the layouts and styles):

We have built our entire search UI in just 60 lines of code!


Step 5: Adding Layout and Styles

ReactiveSearch doesn't use a layout system internally. If you are using a grid from Bootstrap or Materialize, you can use that. Here, will just make use of CSS Flex.

If you are new to Flex, we recommend a quick read of this article.

With ~6 more lines, our final app layout looks as follows.

<ReactiveBase>
    <div style={{ display: "flex", flexDirection: "row" }}>
        <div style={{ display: "flex", flexDirection: "column", width: "30%", margin: "10px" }}>
            <MultiList/>
            <SingleRange/>
        </div>
        <div style={{ display: "flex", flexDirection: "column", width: "66%" }}>
            <SearchBox />
            <ReactiveList />
        </div>
    </div>
</ReactiveBase>

Add some margins between the search and result component, and voila! Our final app is ready:

If you would like to run this setup locally instead, you can clone the ReactiveSearch starter app project.

Summary

In this post, we went through setting OpenSearch and ReactiveSearch API servers, ingesting data into the OpenSearch cluster and then creating a search UI on top of it using the ReactiveSearch UI kit. While we used React for building the search UI here, the steps to follow would be similar for Vue as well.

ย