Fulltext Search with Redis and Next.js

Redis is an in-memory key-value store that is often used as a cache to make traditional databases faster. However, it has evolved into a multimodel database capable of fulltext search, graph relationships, AI workloads, and more.

In the following tutorial, we use Next.js and Redis Enterprise Cloud to build a webapp that can store JSON data in the cloud, then query it with results that update instantly in the UI.

Initial Setup

Sign Up for Redis Cloud

There are many ways to setup Redis, but the quickest option is the free tier on Redis Enterprise Cloud.

Create a new database on Redis Cloud

Create a new database on Redis Cloud

Make sure to add RedisSearch and RedisJSON modules when configuring your database. I also highly recommend downloading Redis Insight desktop app to visualize your data.

Next.js Setup

Create a new next.js app, then install Redis OM - a library that supports object mapping for Redis in Node.js.

command line
npx create-next-app my-app
cd myapp

npm install redis-om

Now, we need to tell Next.js how to connect to the database. Create a file to hold an environment variable called REDIS_URL. Replace the values with config details provided by Redis Enterprise Cloud.

file_type_config .env.local
REDIS_URL=redis://default:PASSWORD@HOST:PORT

Lastly, create a file that initializes the database client and connects to it.

file_type_js lib/redis.js
import { Client, Entity, Schema, Repository } from 'redis-om';

const client = new Client();

async function connect() {
    if (!client.isOpen()) {
        await client.open(process.env.REDIS_URL);
    }
}

Create JSON Documents

The app first needs to store JSON documents in Redis. Each document will represent a car, like a Tesla Model 3 or Toyota Camry.

Define a Schema

Redis OM allows us to provide a consistent schema by extending the Entity class.

file_type_js lib/redis.js
class Car extends Entity {}
let schema = new Schema(
  Car,
  {
    make: { type: 'string' },
    model: { type: 'string' },
    image: { type: 'string' },
    description: { type: 'string', textSearch: true },
  },
  {
    dataStructure: 'JSON',
  }
);

Save a Document

Each entity is managed by the Repository class, which is responsible for saving and retrieving individual documents. When an entity is saved, it is assigned a unique ID as a Redis key, like Car:xyz123.

export async function createCar(data) {
    await connect();
  
    const repository = client.fetchRepository(schema)
  
    const car = repository.createEntity(data);
  
    const id = await repository.save(car);
    return id;
}

Next.js API Route

Implement a Next.js API route to execute the database write on the web.

file_type_js pages/api/cars.js
import { createCar } from '../../lib/redis';

export default async function handler(req, res) {
    const id = await createCar(req.body);
    res.status(200).json({ id })
}

React Form

Implement a React component to collect data from an HTML form.

file_type_js lib/CarForm.js
export default function CarForm() {
    const handleSubmit = async (event) => {
      event.preventDefault();
  
      const form = new FormData(event.target);
      const formData = Object.fromEntries(form.entries());
  
      const res = await fetch('/api/cars', {
        body: JSON.stringify(formData),
        headers: {
          'Content-Type': 'application/json',
        },
        method: 'POST',
      });
  
      const result = await res.json();
      console.log(result)
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <input name="make" type="text"  />
        <input name="model" type="text"  />
        <input name="image" type="text"  />
        <textarea name="description" type="text"  />
  
        <button type="submit">Create Car</button>
      </form>
    );
  }

Now that we can save entities, let’s use RediSearch to index and query our data.

Create an Index

Before we can query our data, we need to create an index. This is a one-time operation that needs to be done whenever the schema changes.

file_type_js lib/redis.js
export async function createIndex() {
    await connect();

    const repository = new Repository(schema, client);
    await repository.createIndex()
}

For convenience, we can create an API route to run this code.

file_type_js pages/api/createIndex.js
import { createIndex } from '../../lib/redis';

export default async function handler(req, res) {
  await createIndex();
  res.status(200).send('ok');
}

Search Query

Use the where method to access keys on each entity. Redis OM makes it easy to chain multiple queries together with or.

file_type_js lib/redis.js
export async function searchCars(q) {
  await connect();

  const repository = new Repository(schema, client);

  const cars = await repository.search()
      .where('make').eq(q)
      .or('model').eq(q)
      .or('description').matches(q)
      .return.all();

  return cars;
}

Now create an API route to execute the search query.

file_type_js pages/api/search.js
import { searchCars } from '../../lib/redis';

export default async function handler(req, res) {
  const q = req.query.q;
  const cars = await searchCars(q);
  res.status(200).json({ cars });
}

Autocomplete Search Form

The final step is to implement a React component to display the results of the search query.

file_type_js lib/SearchForm.js
import { useState } from 'react';

export default function CarForm() {
  const [hits, setHits] = useState([]);

  const search = async (event) => {
    const q = event.target.value;

    if (q.length > 2) {
      const params = new URLSearchParams({ q });

      const res = await fetch('/api/search?' + params);

      const result = await res.json();
      console.log(result);
      setHits(result['cars']);
    }
  };

  return (
    <div>
      <input onChange={search} type="text" />

      <ul>
        {hits.map((hit) => (
            <li key={hit.entityId}>
              {hit.make} {hit.model}
            </li>
          ))}
      </ul>
    </div>
  );
}

Questions? Let's chat

Open Discord