Passing Search A Query Without Using The SearchBox Component In Algolia

You probably haven't noticed (and you'd be forgiven if this is the case!) that my site now has the ability to search through posts. This is a strange turn of events for me as I decided to remove search capability from my site many years ago as I didn't feel it added any benefits for the user. This became evident from Google Analytics stats where searches never hit high enough numbers to warrant having it. The numbers don't lie!

So what caused this turnaround?

I've noticed that I'm regularly referring back through posts to refresh myself on things I've done in the past and to find solutions to issues I know I've previously written about. Having a search would make trawling through my few hundred posts a lot easier. So this is more of a personal requirement than commercial. But there is an exciting aspect to this as well - experimenting with Algolia. Using Algolia search is something I've been meaning to look into for a long time and integrating with GatbsyJS.

The thought of having the good ol' magnifying glass back in the navigation makes me nostalgic!

Note: In this post, I won't be covering the basic Algolia setup or the plugins needed to install as there is already a great wealth of information online. Check out my "Useful Links" section at the end of the post.

Basic Setup

Integrating Algolia into GatbsyJS was relatively straight-forward due to the wealth of information that others have already written and also the plugins themselves. The plugins make light work of rendering search results quickly allowing enough customisations to the HTML markup for easy implementation within any site. By default, the plugins contain the following components:

  • InstantSearch
  • SearchBox
  • Hits
import algoliasearch from 'algoliasearch/lite';
import PropTypes from 'prop-types';
import { Link } from 'gatsby';
import { InstantSearch, Hits, Highlight, SearchBox } from 'react-instantsearch-dom';
import React from 'react';

// Get API keys from the environment file.
const appId = process.env.GATSBY_ALGOLIA_APP_ID;
const searchKey = process.env.GATSBY_ALGOLIA_SEARCH_KEY;
const searchClient = algoliasearch(appId, searchKey);

const SearchPage = () => (
  <InstantSearch
    searchClient={searchClient}
    indexName={process.env.GATSBY_ALGOLIA_INDEX_NAME}
  >
    <SearchBox />
    <Hits hitComponent={Hit} />
  </InstantSearch>
);

function Hit(props) {
  return (
    <article className="hentry post">
      <h3 className="entry-title">
        <Link to={props.hit.fields.slug}>
          <Highlight attribute="title" hit={props.hit} tagName="mark" />
        </Link>
      </h3>
      <div className="entry-meta">
        <span className="read-time">{props.hit.fields.readingTime.text}</span>
      </div>
      <p className="entry-content">
        <Highlight hit={props.hit} attribute="summary" tagName="mark" />
      </p>
    </article>
  );
}

Hit.propTypes = {
  hit: PropTypes.object.isRequired,
};

export default SearchPage;

The InstantSearch is the core component that directly interacts with Algolia's API and takes in two properties, "searchClient" and "indexName" containing the Application ID and Search Key that is acquired from the Algolia account setup. This component contains two child components, SearchBox is the search textbox and Hits that displays results from the search query.

It is the Hits component where we can customise the HTML with our own markup by using it's "hitComponent" attribute. In my case, I created a function to generate HTML where I access the properties from the search index. What's really cool is here is we have the ability to also highlight our search term where they may occur in the results by using the Highlight component (also provided by the Algolia plugin) and adding a "tagName" attribute.

Removing The SearchBox Component

The standard implementation may not suit all scenarios as you may want a search term to be sent to the InstantSearch component differently. For example, it could be from a custom search textbox or (as in my case) read from a query-string parameter. It wasn't until I started delving further into the standard setup I realised you cannot just remove the SearchBox component and pass a value directly, but there is a workaround.

I have expanded upon the code-snippet, above, to demonstrate how my search page works...

import algoliasearch from 'algoliasearch/lite';
import PropTypes from 'prop-types';
import { Link } from 'gatsby';
import { InstantSearch, Hits, Highlight, connectSearchBox } from 'react-instantsearch-dom';
import Layout from "../components/global/layout";
import React, { Component } from "react";

// Get API keys from the environment file.
const appId = process.env.GATSBY_ALGOLIA_APP_ID;
const searchKey = process.env.GATSBY_ALGOLIA_SEARCH_KEY;
const searchClient = algoliasearch(appId, searchKey);
const VirtualSearchBox = connectSearchBox(() => <span />);

class SearchPage extends Component { 
  state = {
    searchState: {
      query: '',
    },
  };

  componentDidMount() {   
    // Get "term" query string parameter value.
    let search = window.location.search;
    let params = new URLSearchParams(search);
    let searchTerm = params.get("term");

    // Send the query string value to a "searchState" object used by Algolia.
    this.setState(state => ({
      searchState: {
        ...state.searchState,
        query: searchTerm,
      },
    }));
 }

  render() {
      // Default "instantSearch" HTML to prompt user to enter a search term.
      var instantSearch = null;
      
      // If there is a search term, utilise Algolia's instant search.
      if (this.state.searchState.query) {
        instantSearch = <div className="entry-content">
                          <h2>You've searched for "{this.state.searchState.query}".</h2>
                          <div className="post-list archives-list">
                          <InstantSearch
                              searchClient={searchClient}
                              indexName={process.env.GATSBY_ALGOLIA_INDEX_NAME}
                              searchState={this.state.searchState}
                            >
                              <VirtualSearchBox />
                              <Hits hitComponent={Hit} />
                            </InstantSearch>  
                          </div>
                        </div>
      }
      else {
        instantSearch = <div className="entry-content">
                          <h2>You haven't entered a search term.</h2>
                          <p>Carry out a search by clicking the <em>magnifying glass</em> in the navigation.</p>
                        </div>
      }

      return (
        <Layout>
          <header className="page-header">
            <h1>Search</h1>
            <p>Search the knowledge-base...</p>
          </header>
          <div id="primary" className="content-area">
            <div id="content" className="site-content" role="main">
                <div className="layout-fixed">
                    <article className="page hentry">
                      {instantSearch}
                    </article>
                </div>
            </div>
          </div>
      </Layout>
    )
  }
}

function Hit(props) {
  return (
    <article className="hentry post">
      <h3 className="entry-title">
        <Link to={props.hit.fields.slug}>
          <Highlight attribute="title" hit={props.hit} tagName="mark" />
        </Link>
      </h3>
      <div className="entry-meta">
        <span className="read-time">{props.hit.fields.readingTime.text}</span>
      </div>
      <p className="entry-content">
        <Highlight hit={props.hit} attribute="summary" tagName="mark" />
      </p>
    </article>
  );
}

Hit.propTypes = {
  hit: PropTypes.object.isRequired,
};

export default SearchPage

My code is reading from a query-string value and passing that to a "searchState". The searchState object is created by React InstantSearch internally. Every widget inside the library has its own way of updating it. It contains parameters on the type of search that should be performed, such as query, sorting and pagination, to name a few. All we're interested in doing is updating the query parameter of this object with our search term.

If the query parameter from the "searchState" object is empty, render search results, otherwise, display a message stating a search term is required.

One thing to notice is the SearchBox has been replaced with a VirtualSearchBox, which uses the connector of the search box to create a virtual widget - in our case an empty span tag. This will link the InstantSearch component with the query. Having some form of search box component is compulsory.

Conclusion

I prefer not to use the out-of-the-box search box component as I can potentially save requests to Algolia's API, as searches aren't being made on the fly as a user enters a search term. This is the plugins default behaviour.

Passing a search term through a query-string may come across as a little backwards, especially when it's rather nice to see search results change before your eyes as you type letter-by-letter. However, this approach misses one key element: Tracking in Google Analytics. Even though I will be primary the person making the most use of my site search, it'll be interesting to see who else uses it and what search keywords are used.

Useful Links