search mobile facets autocomplete spellcheck crawler rankings weights synonyms analytics engage api customize documentation install setup technology content domains user history info home business cart chart contact email activate analyticsalt analytics autocomplete cart contact content crawling custom documentation domains email engage faceted history info install mobile person querybuilder search setup spellcheck synonyms weights engage_search_term engage_related_content engage_next_results engage_personalized_results engage_recent_results success add arrow-down arrow-left arrow-right arrow-up caret-down caret-left caret-right caret-up check close content conversions-small conversions details edit grid help small-info error live magento minus move photo pin plus preview refresh search settings small-home stat subtract text trash unpin wordpress x alert case_deflection advanced-permissions keyword-detection predictive-ai sso

Using App Search in React

You will need an App Search account to follow along. Sign up for a free trial.


Application search has two key dimensions: relevance and user experience. When a user begins a search, they want to find 'the right' content. Whether a user finds exactly what they were looking for, the closest possible option, or something even better, search is not valuable unless it is relevant.

Finding the right content through strong relevance is only part of what will win over a visitor; their search should also feel good. Search should be fast, react to input, and feel intelligent and effective.

This tutorial will teach you about application search by demonstrating how to build a fluid and robust search experience using React and the Elastic App Search JavaScript client. In the end, you will have a good-looking, relevant, React-ified application that will allow you to search over various npm packages in real-time, sorting by facets, with state maintained as part of the URI.

The completed code can be found on GitHub.

The live sample application can be found at http://packagehunt.swiftype.info/.

Requirements

To proceed, you will need to have the following...

  1. A recent version of Node.js.
  2. A recent version of npm.
  3. An App Search account or active free 14-day trial.
  4. About 30 minutes.

App Search, An Introduction

Applications are built around data. Facebook exploded into our social circles by presenting 'friend' data in an interesting way. Amazon started as the most streamlined way of finding and buying books online. Wikipedia made it easy for readers to learn about... well, everything!

Applications exist to solve data problems. In this effort, search is an essential companion. If it is a major application, a key part of its offering will be search: finding friends, products, conversations, or articles. The larger and more interesting the dataset, the more popular the application will be -- especially if search is relevant and rewarding.

App Search is built on top of Elasticsearch, an open-source, distributed, RESTful search engine. Developers receive access to a robust set of API end-points optimized to handle premium application search use cases. Let us see it in action.

Start Your Engines

To begin, create an Engine within App Search.

An Engine ingests objects for indexing. An object is your data; it is the friend profile, the product, or the wiki-page. Once data is ingested into App Search, it is indexed against a flexible schema and optimized for search. From there, we can leverage different client libraries in order to craft an enjoyable search experience.

For this example, we will call our Engine: node-modules.

Once the Engine has been created, we will need three things from the Credentials page:

  1. The Host Identifier, prefixed with host-
  2. A Private API Key, prefixed with private-
  3. A Public Search Key, prefixed with search-

With that, we may clone the project, enter the directory, checkout the starter branch, then run an npm install:

$ git clone https://github.com/swiftype/app-search-demo-react.git
$ cd react-tutorial && git checkout starter && npm install

Great - we have an application primed and ready. But to search requires data...

Ingestion ~

In most cases, your objects would live within a database or a back-end API. Given that this is an example, we will use a static .json file. The repository contains two scripts: init-data.js and index-data.js. The former is a scraper which was used to acquire well-formatted node-module data from npm. The data exists within the node-modules.json file. The latter is an indexer which will ingest that data into your App Search Engine for indexing.

In order to run the indexer script, we will need to pass our Host Identifier and Private API Key along with it.

$ REACT_APP_HOST_IDENTIFIER={Your Host Identifier} \
REACT_APP_API_KEY={Your Private API Key} \
npm run index-data

Objects are rapidly sent to our App Search Engine in batches of 100 and the index is constructed.

We should now have a Dashboard for our newly created Engine with ~9,500 npm packages indexed as documents. It might be useful to poke around with the data so that we are familiar with its contents.

Your new `node-modules` Engine!
Your new node-modules Engine!

Reactivity

With our Engine filled up and ready, we can start building our core application.

$ npm start

Starting npm from within the project directory will open up the React boilerplate. It pulls its styling from App.css - let us better customize it to fit our needs.

In the near future, we are going to need a search box wherein we can type our search queries. Users will seek out these useful rectangles because search engines and browsers have them well trained: type here, find what you want!

// App.css

...

.App-search-box {
  height: 40px;
  width: 500px;
  font-size: 1em;
  margin-top: 10px;
}

We will also need to place our access credentials for App Search in a safe place, like an .env file.

Create one within the project root directory and fill it in, like so:

// .env

REACT_APP_HOST_IDENTIFIER={your host identifier, prefixed with `host-`}
REACT_APP_SEARCH_KEY={your public search key, prefixed with `search-`}

With the variables safely tucked away, we can start writing our search logic.

Starting to Search

The App.js file is where the core logic will live. This file - along with most of the other starter files - was created by create-react-app, a tool to help bootstrap React applications without any configuration. Before we write some logic to test search, we need to install the Swiftype App Search JavaScript client library:

$ npm install --save swiftype-app-search-javascript

Place the following code into App.js. It will perform a basic search.

We will hard-code foo as our example search term:

import * as SwiftypeAppSearch from "swiftype-app-search-javascript";

const client = SwiftypeAppSearch.createClient({
  hostIdentifier: process.env.REACT_APP_HOST_IDENTIFIER,
  apiKey: process.env.REACT_APP_SEARCH_KEY,
  engineName: "node-modules"
});

// We can query for anything -- `foo` is our example.
const query = "foo";
const options = {};
client.search(query, options)
  .then(resultList => console.log(resultList))
  .catch(error => console.log(error))

The browser will refresh and create a resultList array via console.log. To explore the array, we can open up our browser's developer console. We can try a few more queries by replacing the foo query with another string. Once the query is changed and the page refreshes, we can see how the result set has adapted.

Great ~ with that, we are already searching through our node-modules.

Resulting Goodness

We have a simple pattern down in order to search, but the results are of little use hidden away in a console.log. We are going to remove the basic React style and our previous code, then extend things.

We are going to create...

  1. A state variable which will hold a response property.
  2. A performQuery method that will query App Search using client.search. It will store the query results within the response property.
  3. A componentDidMount life-cycle hook, which will run once upon application load. We will query foo again, but we can use any query we would like.
  4. Structured HTML to hold the resulting data output and the total number of results.
// App.js

// ... Truncated!

class App extends Component {
  state = {
    // A new state property, which holds the most recent query response
    response: null
  };

  componentDidMount() {
    /* Calling this in componentDidMount ensures that results are displayed on
    the screen when the app first loads */
    this.performQuery("foo");
  }

  // Method to perform a query and store the response
  performQuery = queryString => {
    client.search(queryString, {}).then(
      response => {
        // Add this for now so you can inspect the full response
        console.log(response);
        this.setState({ response });
      },
      error => {
        console.log(`error: ${error}`);
      }
    );
  };

  render() {
    const {response} = this.state;
    if (!response) return null;

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">Node Module Search</h1>
        </header>
        {/* Show the total count of results for this query */}
        <h2>{response.info.meta.page.total_results} Results</h2>
        {/* Iterate over results, and show their Name and Description */}
        {response.results.map(result => (
          <div key={result.getRaw("id")}>
            <p>Name: {result.getRaw("name")}</p>
            <p>Description: {result.getRaw("description")}</p>
            <br />
          </div>
        ))}
      </div>
    );
  }
}

// ... Truncated!

The moment we hit save, results will appear within http://localhost:3000 -- 27 results and some nifty sounding modules. If something went wrong, we can check out the console as we have two console.logs nested within the code.

Fancy Boxing

We have been hard-coding foo into our queries. What makes search most valuable is that it begins with a free expression. Once you have developed a great search experience, you will be able to optimize for the more common expressions, curating the most relevant result sets. It all begins with a blank canvas: the search box.

To craft an able search box, we will add a property to state called queryString. To keep queryString updated with new strings, we will create an updateQuery method; we will leverage an onChange handler to update queryString and trigger a new search each time a user changes the text in the box.

Our full App class now looks like this:

// src/App.js

// ... Truncated!

class App extends Component {
  state = {
    // A new state property, which tracks value from the search box
    queryString: "",
    response: null
  };

  componentDidMount() {
    // Remove hard-coded search for "node"
    this.performQuery(this.state.queryString);
  }

  // Handles the `onChange` event every time the user types in the search box.
  updateQuery = e => {
    const queryString = e.target.value;
    this.setState(
      {
        queryString // Save the user entered query string
      },
      () => {
        this.performQuery(queryString); // Trigger a new search
      }
    );
  };

  performQuery = queryString => {
    client.search(queryString, {}).then(
      response => {
        this.setState({
          response
        });
      },
      error => {
        console.log(`error: ${error}`);
      }
    );
  };

  render() {
    const {response, queryString} = this.state;
    if (!response) return null;

    return (
      <div className="App">
        <header className="App-header">
          <h1 className="App-title">Node Module Search</h1>
        </header>
        {/* A search box, connected to our query string value and onChange
         handler */}
        <input
          className="App-search-box"
          type="text"
          placeholder="Enter a search term here"
          value={queryString}
          onChange={this.updateQuery}
        />
        <h2>{response.info.meta.page.total_results} Results</h2>
        {response.results.map(result => (
          <div key={result.getRaw("id")}>
            <p>Name: {result.getRaw("name")}</p>
            <p>Description: {result.getRaw("description")}</p>
            <br />
          </div>
        ))}
      </div>
    );
  }
}

// ... Truncated!

Debounce!

Within this iteration, each time a change is detected within the box a search will occur -- that might get intensive on our systems. To remedy this we will apply a debounce function, courtesy of Lodash.

$ npm install --save lodash

Debounce is a method of rate-limiting the number of inbound requests based on a defined number of milliseconds. A user is thinking about how to phrase their query, making typos, or typing very fast... and so, we do not need to query on every detected change.

By wrapping our performQuery method within a debounce function from Lodash, we can specify a 200ms rate-limit - 200ms must elapse with no input before the next search query begins:

// App.js
// ... Truncated!
import { debounce } from "lodash"; // Import debounce

// ... Truncated!

performQuery = debounce(queryString => {
  client.search(queryString, {}).then(
    response => {
      this.setState({
        response
      });
    },
    error => {
      console.log(`error: ${error}`);
    }
  );
}, 200); // 200 milliseconds.

// ... Truncated!

Apart from giving our servers a break, rate-limiting can help smooth out user queries. It goes a long way! Feel is important.

Next Up...

This is the beginning of a quality, React-ified search experience. Going forward, there are many great things to add. We could add styling or implement dynamic App Search features like Facets, Curations, or Relevance Tuning. Or, we could explore the Analytics API Suite to unearth valuable insights into user search activity.

If we wanted to get really deep, the README within the master branch extends the tutorial to craft URI-based state management, add slick styling, pagination, filtering, and faceted search. With a few stylistic customizations, it could become the foundation for a high-quality search experience.

The fully developed sample is live here: http://packagehunt.swiftype.info/.

Keep Going!
Package Search teaser

Summary

In the past, constructing relevant and rewarding application search has been complicated. App Search is an expedient, managed way to write valuable, tunable search into web applications. The best part? Both Engineers and less-technical stakeholders can manage key features from within a slick and intuitive dashboard. You can jump right into App Search with a free 14-day trial -- no credit card required.