Next.js/Contentful/Algolia Full-Text Search

In the following tutorial, we'll be learning how to implement full-text search using Next.js, Contentful, and Algolia.  The results will be displayed in real-time as the search box input changes. I've decided not to include any styling in this tutorial, but feel free to add stylig as you go if you feel like it. 

##Prerequisites

+ A Next.js project: You can either use an existing project or create a new one by running `npx create-next-app` or `yarn create-next-app`.
+ A Contentful account, data, Space Id and Access Token: If you're new to Contentful, learn how to get your API keys and set up and fetch your data [here](https://qiita.com/Code-Kat/items/ff4fbc60098858785ea2#setting-up-contentful).
+ An Algolia account: Create a free account [here](https://www.algolia.com/).
+ A local.env file: Create the file as the root of your project and add your Contentful `SPACE_ID` and `ACCESS_TOKEN` to it. 

##Setting up Algolia

##Writing the Script

At the root of your project, create a `scripts` folder. Inside the folder, create a file named `build-search.js`. Paste the code below into your file. Install `graphql-request` and `algoliasearch/lite` either through yarn or npm. If your Contentful data includes rich text, also install `@contentful/rich-text-plain-text-renderer`.

Add NEXT_PUBLIC_ALGOLIA_APP_ID and ALGOLIA_SEARCH_ADMIN_KEY to your `env.local` file. To get the keys, from your Algolia dashboard, click "API keys" in the menu on the left.  Grab the "Search-Only API Key" and the "Admin API Key".


```js:scripts/build-search.js
//I added the following two lines because my env variables 
//weren't working
const path = require("path");
require("dotenv").config({ path: path.resolve(__dirname, "../.env.local") });

//To handle rich text
const richTextPlainTextRenderer = require("@contentful/rich-text-plain-text-renderer");

//To fetch Contentful data
const { GraphQLClient, gql } = require("graphql-request");

const algoliasearch = require("algoliasearch/lite");

//Fetch Contentful data
const callContentful = async () => {
  const endpoint = `https://graphql.contentful.com/content/v1/spaces/${process.env.SPACE_ID}`;

  const graphQLClient = new GraphQLClient(endpoint, {
    headers: {
      authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
    },
  });

  const query = gql`
    <your GraphQL query goes here>
  `;
  const data = await graphQLClient.request(query);
  //returns array of posts
  return data.bluecodeArticleCollection.items;
};

//Algolia accepts data in JSON format
function transformPostsToSearchObjects(posts) {
  const transformed = posts.map((post) => {
//This is just an example
//Customise the code below according to your query data
    return {
      objectID: post.sys.id,
      title: post.title,
      slug: post.slug,
      genre: post.genre,
      date: post.date,
      body: richTextPlainTextRenderer.documentToPlainTextString(
        post.article.json
      ),
    };
  });
  return transformed;
}

(async function () {
  try {
    const posts = await callContentful();
    const transformed = transformPostsToSearchObjects(posts);

    const client = algoliasearch(
      process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
      process.env.ALGOLIA_SEARCH_ADMIN_KEY
    );
//you can can chose any name you like instead of 
//"full_text_search"
    const index = client.initIndex("full_text_search");

    const algoliaResponse = await index.saveObjects(transformed);

    console.log(
      `🎉 Success! You added ${
        algoliaResponse.objectIDs.length
      } records to Algolia search. Object IDs:\n${algoliaResponse.objectIDs.join(
        "\n"
      )}`
    );
  } catch (error) {
    console.log(error);
  }
})();

```

Now add the following like to your `scripts` object in `package.json`: `"postbuild": "node ./scripts/build-search.js"`. Your scripts object should look like this:

```json:package.json
"scripts": {
    "dev": "next dev",
    "build": "next build",
    "postbuild": "node ./scripts/build-search.js",
    "start": "next start",
    "lint": "next lint"
  }
```
Your script will now run automatically on every build. 

Now, let's make sure everything is working as expected. Run `node ./scripts/build-search.js`. If you get a success message in your terminal, it means your Contentful data has been saved to Algolia and is ready to be searched. 

##Creating the Layout

It's time to move on the the frontend of our app. Open pages/index.js and replace the file's contents with the code below.

```react:pages/index.js
import algoliasearch from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch-dom";
//We'll create this file in the next step
import SearchBox from "../components/SearchBox";

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY
);

const Search = () => {
  return (
    <>
      {/* replace "full_text_search" with the name you used when 
      indexing your data in scripts/build-search.js*/}
      <InstantSearch searchClient={searchClient} indexName="full_text_seach">
        {/* We'll create the search box in the next step */}
        <SearchBox />
      </InstantSearch>
    </>
  );
};

export default Search;
```

##Creating the Search Box

At the root of your project, create a `components` folder. Create `SearchBox.js` in your new folder and add the following code to it:

```react:components/SearchBox.js
import { useState } from "react";
import { connectSearchBox } from "react-instantsearch-dom";
// We'll create Hits in the next step
import Hits from "./Hits";

function SearchBox({ refine }) {
  const [searchTerm, setSearchTerm] = useState("");

// search for matches when even the input field values changes
  const handleChange = (e) => {
    setSearchTerm(e.target.value);
    refine(e.currentTarget.value);
  };
  return (
   <div>
     <input
       value={searchTerm}
       placeholder="Search Blog Posts"
       onChange={handleChange}
     />
     {/* We'll create Hits in the next step */}
     <Hits />
   </div>
  );
}

export default connectSearchBox(SearchBox);
```

##Diplaying the Results

Create a new file, Hits.js, in the components folder:

```react:Hits.js

import { connectStateResults } from "react-instantsearch-dom";

const ResultsHits = ({ searchState, searchResults }) => {
  //The user will need to enter at least two characters 
  //before they can see search results. Feel free to 
  //change this number.
  const validQuery = searchState.query?.length >= 2;

  return (
    <>
      {/* Show if the user enters less than two characters  */}
      {!validQuery && <p>Please enter at least 2 characters</p>}

      {/* Show if no results match the user's input */}
      {searchResults?.hits.length === 0 && validQuery && (
        <p>No matching results, sorry.</p>
      )}

      {/* Show results  */}
      {searchResults?.hits.length > 0 && validQuery && (
        <div style={{ padding: "50px 0 0 0 " }}>
          {console.log(searchResults.hits)}
          {searchResults.hits.map((hit) => (

           // display your hits in any way you like here
            <p>{hit.title}</p>
          ))}
        </div>
      )}
    </>
  );
};

export default connectStateResults(ResultsHits);

```

##Wrapping Up

Hopefully you now have a working search box that displays results in real-time as you type your search query. Depending on your use case, you could play with displaying the search results directly below the search box (Google style), or showing them on a separate results page. You can also customize you visiter's search experience in your Algolia dashboard.  If you have any questions, leave a comment and I'll do my best to answer. 

Happy Coding!