title: RedisOM for Node.js

summary: Learn how to build with Redis Stack and Node.js

title: Overview

text: This tutorial will show you how to build a simple, RESTful API that reads, writes, and finds data on persons (including first name, last name, and  age) using Node.js and Redis Stack. You'll also add a simple location tracking feature for a bit of extra interest. You'll be using [Express](https://expressjs.com/) and [Redis OM](https://github.com/redis/redis-om-node) to do this.

title: What is Redis OM?

text:
Redis OM is an object-mapping library that provides high-level abstractions for working with Redis as a document database. You can use Redis OM to map Redis data types,specifically Hashes and JSON documents, to objects in your preferred programming language.

Redis OM leverages the JSON and Redis Search features included in Redis Stack, enabling you to:

- **Model your data** as objects with schemas and validation
- **Store and retrieve** complex data structures seamlessly
- **Search and query** your data using full-text search, numeric ranges, and geospatial queries
- **Index your data** automatically for optimal query performance

With Redis OM, you work with familiar object-oriented patterns while Redis Stack handles the underlying data storage, indexing, and querying efficiently.

Redis Stack was replaced by Redis Open Source as of version 8.0.

title: Prerequisites

text:
Like anything software-related, you need to have some dependencies installed before you can get started:

- [Node.js 14.8+](https://nodejs.org/en/): In this tutorial, we're using JavaScript's top-level `await` feature which was introduced in Node 14.8. So, make sure you are using that version or later.
- [Redis Stack](): You need a version of Redis Stack, either running locally on your machine or [in the cloud](https://redis.com/try-free/?utm_source=redisio&utm_medium=referral&utm_campaign=2023-09-try_free&utm_content=cu-redis_cloud_users).
- [Redis Insight](https://redis.com/redis-enterprise/redis-insight/): We'll use this to look inside Redis and make sure our code is doing what we think it's doing.

title: Starter code

text:
We're not going to code this completely from scratch. Instead, we've provided some starter code for you. Go ahead and clone it to a folder of your convenience:

    git clone git@github.com:redis-developer/express-redis-om-workshop.git

Now that you have the starter code, let's explore it a bit. Opening up `server.js` in the root we see that we have a simple Express app that uses [*Dotenv*](https://www.npmjs.com/package/dotenv) for configuration and [Swagger UI Express](https://www.npmjs.com/package/swagger-ui-express) for testing our API:

import 'dotenv/config'

import express from 'express'
import swaggerUi from 'swagger-ui-express'
import YAML from 'yamljs'

/* create an express app and use JSON */
const app = new express()
app.use(express.json())

/* set up swagger in the root */
const swaggerDocument = YAML.load('api.yaml')
app.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument))

/* start the server */
app.listen(8080)

Alongside this is `api.yaml`, which defines the API we're going to build and provides the information Swagger UI Express needs to render its UI. You don't need to mess with it unless you want to add some additional routes.

The `persons` folder has some JSON files and a shell script. The JSON files are sample persons—all musicians because fun—that you can load into the API to test it. The shell script—`load-data.sh`—will load all the JSON files into the API using `curl`.

There are two empty folders, `om` and `routers`. The `om` folder is where all the Redis OM code will go. The `routers` folder will hold code for all of our Express routes.

title: Configure and run

text:
The starter code is perfectly runnable if a bit thin. Let's configure and run it to make sure it works before we move on to writing actual code. First, get all the dependencies:

    npm install

Then, set up a `.env` file in the root that Dotenv can make use of. There's a `sample.env` file in the root that you can copy and modify:

    cp sample.env .env

The contents of `.env` looks like this:

# Put your local Redis Stack URL here. Want to run in the
# cloud instead? Sign up at https://redis.com/try-free/.
REDIS_URL=redis://localhost:6379

There's a good chance this is already correct. However, if you need to change the `REDIS_URL` for your particular environment (e.g., you're running Redis Stack in the cloud), this is the time to do it. Once done, you should be able to run the app:

    npm start

Navigate to `http://localhost:8080` and check out the client that Swagger UI Express has created. None of it *works* yet because we haven't implemented any of the routes. But, you can try them out and watch them fail!

The starter code runs. Let's add some Redis OM to it so it actually *does* something!

title: Setting up a Client

text:
First things first, let's set up a **client**. The `Client` class is the thing that knows how to talk to Redis on behalf of Redis OM. One option is to put our client in its own file and export it. This ensures that the application has one and only one instance of `Client` and thus only one connection to Redis Stack. Since Redis and JavaScript are both (more or less) single-threaded, this works neatly.

Let's create our first file. In the `om` folder add a file called `client.js` and add the following code:

import { Client } from 'redis-om'

/* pulls the Redis URL from .env */
const url = process.env.REDIS_URL

/* create and open the Redis OM Client */
const client = await new Client().open(url)

export default client

> Remember that _top-level await_ stuff we mentioned earlier? There it is!

Note that we are getting our Redis URL from an environment variable. It was put there by Dotenv and read from our `.env` file. If we didn't have the `.env` file or have a `REDIS_URL` property in our `.env` file, this code would gladly read this value from the *actual* environment variables.

Also note that the `.open()` method conveniently returns `this`. This `this` (can I say *this* again? I just did!) lets us chain the instantiation of the client with the opening of the client. If this isn't to your liking, you could always write it like this:

/* create and open the Redis OM Client */
const client = new Client()
await client.open(url)

title: Entity, Schema, and Repository

text:
Now that we have a client that's connected to Redis, we need to start mapping some persons. To do that, we need to define an `Entity` and a `Schema`. Let's start by creating a file named `person.js` in the `om` folder and importing `client` from `client.js` and the `Entity` and `Schema` classes from Redis OM:

import { Entity, Schema } from 'redis-om'
import client from './client.js'

title: Entity

text:
Next, we need to define an **entity**. An `Entity` is the class that holds you data when you work with it—the thing being mapped to. It is what you create, read, update, and delete. Any class that extends `Entity` is an entity. We'll define our `Person` entity with a single line:

/* our entity */
class Person extends Entity {}

title: Schema

text:
A **schema** defines the fields on your entity, their types, and how they are mapped internally to Redis. By default, entities map to JSON documents. Let's create our `Schema` in `person.js`:

/* create a Schema for Person */
const personSchema = new Schema(Person, {
  firstName: { type: 'string' },
  lastName: { type: 'string' },
  age: { type: 'number' },
  verified: { type: 'boolean' },
  location: { type: 'point' },
  locationUpdated: { type: 'date' },
  skills: { type: 'string[]' },
  personalStatement: { type: 'text' }
})

When you create a `Schema`, it modifies the `Entity` class you handed it (`Person` in our case) adding getters and setters for the properties you define. The type those getters and setters accept and return are defined with the type parameter as shown above. Valid values are: `string`, `number`, `boolean`, `string[]`, `date`, `point`, and `text`.

The first three do exactly what you think—they define a property that is a [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), a [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number), or a [`Boolean`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean). `string[]` does what you'd think as well, specifically defining an [`Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of strings.

`date` is a little different, but still more or less what you'd expect. It defines a property that returns a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) and can be set using not only a `Date` but also a `String` containing an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date or a `Number` with the [UNIX epoch time](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_ecmascript_epoch_and_timestamps) in *milliseconds*.

A `point` defines a point somewhere on the globe as a longitude and a latitude. It creates a property that returns and accepts a simple object with the properties of `longitude` and `latitude`. Like this:

let point = { longitude: 12.34, latitude: 56.78 }

A `text` field is a lot like a `string`. If you're just reading and writing objects, they are identical. But if you want to *search* on them, they are very, very different. We'll talk about search more later, but the tl;dr is that `string` fields can only be matched on their whole value—no partial matches—and are best for keys while `text` fields have full-text search enabled on them and are optimized for human-readable text.

title: Repository

text:
Now we have all the pieces that we need to create a **repository**. A `Repository` is the main interface into Redis OM. It gives us the methods to read, write, and remove a specific `Entity`. Create a `Repository` in `person.js` and make sure it's exported as you'll need it when we start implementing out API:

/* use the client to create a Repository just for Persons */
export const personRepository = new Repository(personSchema, client)

We're almost done with setting up our repository. But we still need to create an index or we won't be able to search. We do that by calling `.createIndex()`. If an index already exists and it's identical, this function won't do anything. If it's different, it'll drop it and create a new one. Add a call to `.createIndex()` to `person.js`:

/* create the index for Person */
await personRepository.createIndex()

That's all we need for `person.js` and all we need to start talking to Redis using Redis OM. Here's the code in its entirety:

import { Entity, Schema } from 'redis-om'
import client from './client.js'

/* our entity */
class Person extends Entity {}

/* create a Schema for Person */
const personSchema = new Schema(Person, {
  firstName: { type: 'string' },
  lastName: { type: 'string' },
  age: { type: 'number' },
  verified: { type: 'boolean' },
  location: { type: 'point' },
  locationUpdated: { type: 'date' },
  skills: { type: 'string[]' },
  personalStatement: { type: 'text' }
})

/* use the client to create a Repository just for Persons */
export const personRepository = client.fetchRepository(personSchema)

/* create the index for Person */
await personRepository.createIndex()

Now, let's add some routes in Express.

title: Set up the Person Router

text:
Let's create a truly RESTful API with the CRUD operations mapping to PUT, GET, POST, and DELETE respectively. We're going to do this using [Express Routers](https://expressjs.com/en/4x/api.html#router) as this makes our code nice and tidy. Create a file called `person-router.js` in the `routers` folder and in it import `Router` from Express and `personRepository` from `person.js`. Then create and export a `Router`:

import { Router } from 'express'
import { personRepository } from '../om/person.js'

export const router = Router()

Imports and exports done, let's bind the router to our Express app. Open up `server.js` and import the `Router` we just created:

/* import routers */
import { router as personRouter } from './routers/person-router.js'

Then add the `personRouter` to the Express app:

/* bring in some routers */
app.use('/person', personRouter)

Your `server.js` should now look like this:

import 'dotenv/config'

import express from 'express'
import swaggerUi from 'swagger-ui-express'
import YAML from 'yamljs'

/* import routers */
import { router as personRouter } from './routers/person-router.js'

/* create an express app and use JSON */
const app = new express()
app.use(express.json())

/* bring in some routers */
app.use('/person', personRouter)

/* set up swagger in the root */
const swaggerDocument = YAML.load('api.yaml')
app.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument))

/* start the server */
app.listen(8080)

Now we can add our routes to create, read, update, and delete persons. Head back to the `person-router.js` file so we can do just that.

title: Creating a Person

text:
We'll create a person first as you need to have persons in Redis before you can do any of the reading, writing, or removing of them. Add the PUT route below. This route will call `.createAndSave()` to create a `Person` from the request body and immediately save it to the Redis:

router.put('/', async (req, res) => {
  const person = await personRepository.createAndSave(req.body)
  res.send(person)
})

Note that we are also returning the newly created `Person`. Let's see what that looks like by actually calling our API using the Swagger UI. Go to http://localhost:8080 in your browser and try it out. The default request body in Swagger will be fine for testing. You should see a response that looks like this:

{
  "entityId": "01FY9MWDTWW4XQNTPJ9XY9FPMN",
  "firstName": "Rupert",
  "lastName": "Holmes",
  "age": 75,
  "verified": false,
  "location": {
    "longitude": 45.678,
    "latitude": 45.678
  },
  "locationUpdated": "2022-03-01T12:34:56.123Z",
  "skills": [
    "singing",
    "songwriting",
    "playwriting"
  ],
  "personalStatement": "I like piña coladas and walks in the rain"
}

This is exactly what we handed it with one exception: the `entityId`. Every entity in Redis OM has an entity ID which is—as you've probably guessed—the unique ID of that entity. It was randomly generated when we called `.createAndSave()`. Yours will be different, so make note of it.

You can see this newly created JSON document in Redis with Redis Insight. Go ahead and launch Redis Insight and you should see a key with a name like `Person:01FY9MWDTWW4XQNTPJ9XY9FPMN`. The `Person` bit of the key was derived from the class name of our entity and the sequence of letters and numbers is our generated entity ID. Click on it to take a look at the JSON document you've created.

You'll also see a key named `Person:index:hash`. That's a unique value that Redis OM uses to see if it needs to recreate the index or not when `.createIndex()` is called. You can safely ignore it.

title: Reading a Person

text:
Create down, let's add a GET route to read this newly created `Person`:

router.get('/:id', async (req, res) => {
  const person = await personRepository.fetch(req.params.id)
  res.send(person)
})

This code extracts a parameter from the URL used in the route—the `entityId` that we received previously. It uses the `.fetch()` method on the `personRepository` to retrieve a `Person` using that `entityId`. Then, it returns that `Person`.

Let's go ahead and test that in Swagger as well. You should get back exactly the same response. In fact, since this is a simple GET, we should be able to just load the URL into our browser. Test that out too by navigating to http://localhost:8080/person/01FY9MWDTWW4XQNTPJ9XY9FPMN, replacing the entity ID with your own.

Now that we can read and write, let's implement the *REST* of the HTTP verbs. REST... get it?

title: Updating a Person

text:
Let's add the code to update a person using a POST route:

router.post('/:id', async (req, res) => {

  const person = await personRepository.fetch(req.params.id)

  person.firstName = req.body.firstName ?? null
  person.lastName = req.body.lastName ?? null
  person.age = req.body.age ?? null
  person.verified = req.body.verified ?? null
  person.location = req.body.location ?? null
  person.locationUpdated = req.body.locationUpdated ?? null
  person.skills = req.body.skills ?? null
  person.personalStatement = req.body.personalStatement ?? null

  await personRepository.save(person)

  res.send(person)
})

This code fetches the `Person` from the `personRepository` using the `entityId` just like our previous route did. However, now we change all the properties based on the properties in the request body. If any of them are missing, we set them to `null`. Then, we call `.save()` and return the changed `Person`.

Let's test this in Swagger too, why not? Make some changes. Try removing some of the fields. What do you get back when you read it after you've changed it?

title: Deleting a Person

text:
Deletion—my favorite! Remember kids, deletion is 100% compression. The route that deletes is just as straightforward as the one that reads, but much more destructive:

router.delete('/:id', async (req, res) => {
  await personRepository.remove(req.params.id)
  res.send({ entityId: req.params.id })
})

I guess we should probably test this one out too. Load up Swagger and exercise the route. You should get back JSON with the entity ID you just removed:

{
  "entityId": "01FY9MWDTWW4XQNTPJ9XY9FPMN"
}

And just like that, it's gone!

title: All the CRUD

text:
Do a quick check with what you've written so far. Here's what should be the totality of your `person-router.js` file:

import { Router } from 'express'
import { personRepository } from '../om/person.js'

export const router = Router()

router.put('/', async (req, res) => {
  const person = await personRepository.createAndSave(req.body)
  res.send(person)
})

router.get('/:id', async (req, res) => {
  const person = await personRepository.fetch(req.params.id)
  res.send(person)
})

router.post('/:id', async (req, res) => {

  const person = await personRepository.fetch(req.params.id)

  person.firstName = req.body.firstName ?? null
  person.lastName = req.body.lastName ?? null
  person.age = req.body.age ?? null
  person.verified = req.body.verified ?? null
  person.location = req.body.location ?? null
  person.locationUpdated = req.body.locationUpdated ?? null
  person.skills = req.body.skills ?? null
  person.personalStatement = req.body.personalStatement ?? null

  await personRepository.save(person)

  res.send(person)
})

router.delete('/:id', async (req, res) => {
  await personRepository.remove(req.params.id)
  res.send({ entityId: req.params.id })
})

title: Preparing to search

text:
CRUD completed, let's do some searching. In order to search, we need data to search over. Remember that `persons` folder with all the JSON documents and the `load-data.sh` shell script? Its time has arrived. Go into that folder and run the script:

    cd persons
    ./load-data.sh

You should get a rather verbose response containing the JSON response from the API and the names of the files you loaded. Like this:

{"entityId":"01FY9Z4RRPKF4K9H78JQ3K3CP3","firstName":"Chris","lastName":"Stapleton","age":43,"verified":true,"location":{"longitude":-84.495,"latitude":38.03},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["singing","football","coal mining"],"personalStatement":"There are days that I can walk around like I'm alright. And I pretend to wear a smile on my face. And I could keep the pain from comin' out of my eyes. But sometimes, sometimes, sometimes I cry."} <- chris-stapleton.json
{"entityId":"01FY9Z4RS2QQVN4XFYSNPKH6B2","firstName":"David","lastName":"Paich","age":67,"verified":false,"location":{"longitude":-118.25,"latitude":34.05},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["singing","keyboard","blessing"],"personalStatement":"I seek to cure what's deep inside frightened of this thing that I've become"} <- david-paich.json
{"entityId":"01FY9Z4RSD7SQMSWDFZ6S4M5MJ","firstName":"Ivan","lastName":"Doroschuk","age":64,"verified":true,"location":{"longitude":-88.273,"latitude":40.115},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["singing","dancing","friendship"],"personalStatement":"We can dance if we want to. We can leave your friends behind. 'Cause your friends don't dance and if they don't dance well they're no friends of mine."} <- ivan-doroschuk.json
{"entityId":"01FY9Z4RSRZFGQ21BMEKYHEVK6","firstName":"Joan","lastName":"Jett","age":63,"verified":false,"location":{"longitude":-75.273,"latitude":40.003},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["singing","guitar","black eyeliner"],"personalStatement":"I love rock n' roll so put another dime in the jukebox, baby."} <- joan-jett.json
{"entityId":"01FY9Z4RT25ABWYTW6ZG7R79V4","firstName":"Justin","lastName":"Timberlake","age":41,"verified":true,"location":{"longitude":-89.971,"latitude":35.118},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["singing","dancing","half-time shows"],"personalStatement":"What goes around comes all the way back around."} <- justin-timberlake.json
{"entityId":"01FY9Z4RTD9EKBDS2YN9CRMG1D","firstName":"Kerry","lastName":"Livgren","age":72,"verified":false,"location":{"longitude":-95.689,"latitude":39.056},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["poetry","philosophy","songwriting","guitar"],"personalStatement":"All we are is dust in the wind."} <- kerry-livgren.json
{"entityId":"01FY9Z4RTR73HZQXK83JP94NWR","firstName":"Marshal","lastName":"Mathers","age":49,"verified":false,"location":{"longitude":-83.046,"latitude":42.331},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["rapping","songwriting","comics"],"personalStatement":"Look, if you had, one shot, or one opportunity to seize everything you ever wanted, in one moment, would you capture it, or just let it slip?"} <- marshal-mathers.json
{"entityId":"01FY9Z4RV2QHH0Z1GJM5ND15JE","firstName":"Rupert","lastName":"Holmes","age":75,"verified":true,"location":{"longitude":-2.518,"latitude":53.259},"locationUpdated":"2022-01-01T12:00:00.000Z","skills":["singing","songwriting","playwriting"],"personalStatement":"I like piña coladas and taking walks in the rain."} <- rupert-holmes.json

A little messy, but if you don't see this, then it didn't work!

Now that we have some data, let's add another router to hold the search routes we want to add. Create a file named `search-router.js` in the routers folder and set it up with imports and exports just like we did in `person-router.js`:

import { Router } from 'express'
import { personRepository } from '../om/person.js'

export const router = Router()

Import the `Router` into `server.js` the same way we did for the `personRouter`:

/* import routers */
import { router as personRouter } from './routers/person-router.js'
import { router as searchRouter } from './routers/search-router.js'

Then add the `searchRouter` to the Express app:

/* bring in some routers */
app.use('/person', personRouter)
app.use('/persons', searchRouter)

Router bound, we can now add some routes.

title: Search all the things

text:
We're going to add a plethora of searches to our new `Router`. But the first will be the easiest as it's just going to return everything. Go ahead and add the following code to `search-router.js`:

router.get('/all', async (req, res) => {
  const persons = await personRepository.search().return.all()
  res.send(persons)
})

Here we see how to start and finish a search. Searches start just like CRUD operations start—on a `Repository`. But instead of calling `.createAndSave()`, `.fetch()`, `.save()`, or `.remove()`, we call `.search()`. And unlike all those other methods, `.search()` doesn't end there. Instead, it allows you to build up a query (which you'll see in the next example) and then resolve it with a call to `.return.all()`.

With this new route in place, go into the Swagger UI and exercise the `/persons/all` route. You should see all of the folks you added with the shell script as a JSON array.

In the example above, the query is not specified—we didn't build anything up. If you do this, you'll just get everything. Which is what you want sometimes. But not most of the time. It's not really searching if you just return everything. So let's add a route that lets us find persons by their last name. Add the following code:

router.get('/by-last-name/:lastName', async (req, res) => {
  const lastName = req.params.lastName
  const persons = await personRepository.search()
    .where('lastName').equals(lastName).return.all()
  res.send(persons)
})

In this route, we're specifying a field we want to filter on and a value that it needs to equal. The field name in the call to `.where()` is the name of the field specified in our schema. This field was defined as a `string`, which matters because the type of the field determines the methods that are available query it.

In the case of a `string`, there's just `.equals()`, which will query against the value of the entire string. This is aliased as `.eq()`, `.equal()`, and `.equalTo()` for your convenience. You can even add a little more syntactic sugar with calls to `.is` and `.does` that really don't do anything but make your code pretty. Like this:

const persons = await personRepository.search().where('lastName').is.equalTo(lastName).return.all()
const persons = await personRepository.search().where('lastName').does.equal(lastName).return.all()

You can also invert the query with a call to `.not`:

const persons = await personRepository.search().where('lastName').is.not.equalTo(lastName).return.all()
const persons = await personRepository.search().where('lastName').does.not.equal(lastName).return.all()

In all these cases, the call to `.return.all()` executes the query we build between it and the call to `.search()`. We can search on other field types as well. Let's add some routes to search on a `number` and a `boolean` field:

router.get('/old-enough-to-drink-in-america', async (req, res) => {
  const persons = await personRepository.search()
    .where('age').gte(21).return.all()
  res.send(persons)
})

router.get('/non-verified', async (req, res) => {
  const persons = await personRepository.search()
    .where('verified').is.not.true().return.all()
  res.send(persons)
})

The `number` field is filtering persons by age where the age is great than or equal to 21. Again, there are aliases and syntactic sugar:

const persons = await personRepository.search().where('age').is.greaterThanOrEqualTo(21).return.all()

But there are also more ways to query:

const persons = await personRepository.search().where('age').eq(21).return.all()
const persons = await personRepository.search().where('age').gt(21).return.all()
const persons = await personRepository.search().where('age').gte(21).return.all()
const persons = await personRepository.search().where('age').lt(21).return.all()
const persons = await personRepository.search().where('age').lte(21).return.all()
const persons = await personRepository.search().where('age').between(21, 65).return.all()

The `boolean` field is searching for persons by their verification status. It already has some of our syntactic sugar in it. Note that this query will match a missing value or a false value. That's why I specified `.not.true()`. You can also call `.false()` on boolean fields as well as all the variations of `.equals`.

const persons = await personRepository.search().where('verified').true().return.all()
const persons = await personRepository.search().where('verified').false().return.all()
const persons = await personRepository.search().where('verified').equals(true).return.all()

> So, we've created a few routes and I haven't told you to test them. Maybe you have anyhow. If so, good for you, you rebel. For the rest of you, why don't you go ahead and test them now with Swagger? And, going forward, just test them when you want. Heck, create some routes of your own using the provided syntax and try those out too. Don't let me tell you how to live your life.

Of course, querying on just one field is never enough. Not a problem, Redis OM can handle `.and()` and `.or()` like in this route:

router.get('/verified-drinkers-with-last-name/:lastName', async (req, res) => {
  const lastName = req.params.lastName
  const persons = await personRepository.search()
    .where('verified').is.true()
      .and('age').gte(21)
      .and('lastName').equals(lastName).return.all()
  res.send(persons)
})

Here, I'm just showing the syntax for `.and()` but, of course, you can also use `.or()`.

title: Full-text search

text:
If you've defined a field with a type of `text` in your schema, you can perform full-text searches against it. The way a `text` field is searched is different from how a `string` is searched. A `string` can only be compared with `.equals()` and must match the entire string. With a `text` field, you can look for words within the string.

A `text` field is optimized for human-readable text, like an essay or song lyrics. It's pretty clever. It understands that certain words (like *a*, *an*, or *the*) are common and ignores them. It understands how words are grammatically similar and so if you search for *give*, it matches *gives*, *given*, *giving*, and *gave* too. And it ignores punctuation.

Let's add a route that does full-text search against our `personalStatement` field:

router.get('/with-statement-containing/:text', async (req, res) => {
  const text = req.params.text
  const persons = await personRepository.search()
    .where('personalStatement').matches(text)
      .return.all()
  res.send(persons)
})

Note the use of the `.matches()` function. This is the only one that works with `text` fields. It takes a string that can be one or more words—space-delimited—that you want to query for. Let's try it out. In Swagger, use this route to search for the word "walk". You should get the following results:

[
  {
    "entityId": "01FYC7CTR027F219455PS76247",
    "firstName": "Rupert",
    "lastName": "Holmes",
    "age": 75,
    "verified": true,
    "location": {
      "longitude": -2.518,
      "latitude": 53.259
    },
    "locationUpdated": "2022-01-01T12:00:00.000Z",
    "skills": [
      "singing",
      "songwriting",
      "playwriting"
    ],
    "personalStatement": "I like piña coladas and taking walks in the rain."
  },
  {
    "entityId": "01FYC7CTNBJD9CZKKWPQEZEW14",
    "firstName": "Chris",
    "lastName": "Stapleton",
    "age": 43,
    "verified": true,
    "location": {
      "longitude": -84.495,
      "latitude": 38.03
    },
    "locationUpdated": "2022-01-01T12:00:00.000Z",
    "skills": [
      "singing",
      "football",
      "coal mining"
    ],
    "personalStatement": "There are days that I can walk around like I'm alright. And I pretend to wear a smile on my face. And I could keep the pain from comin' out of my eyes. But sometimes, sometimes, sometimes I cry."
  }
]

Notice how the word "walk" is matched for Rupert Holmes' personal statement that contains "walks" *and* matched for Chris Stapleton's that contains "walk". Now search "walk raining". You'll see that this returns Rupert's entry only even though the exact text of neither of these words is found in his personal statement. But they are *grammatically* related so it matched them. This is called stemming and it's a pretty cool feature of Redis Stack that Redis OM exploits.

And if you search for "a rain walk" you'll *still* match Rupert's entry even though the word "a" is not in the text. Why? Because it's a common word that's not very helpful with searching. These common words are called stop words and this is another cool feature of Redis Stack that Redis OM just gets for free.

title: Searching the globe

text:
Redis Stack, and therefore Redis OM, both support searching by geographic location. You specify a point in the globe, a radius, and the units for that radius and it'll gleefully return all the entities therein. Let's add a route to do just that:

router.get('/near/:lng,:lat/radius/:radius', async (req, res) => {
  const longitude = Number(req.params.lng)
  const latitude = Number(req.params.lat)
  const radius = Number(req.params.radius)

  const persons = await personRepository.search()
    .where('location')
      .inRadius(circle => circle
          .longitude(longitude)
          .latitude(latitude)
          .radius(radius)
          .miles)
        .return.all()

  res.send(persons)
})

This code looks a little different than the others because the way we define the circle we want to search is done with a function that is passed into the `.inRadius` method:

circle => circle.longitude(longitude).latitude(latitude).radius(radius).miles

All this function does is accept an instance of a [`Circle`](https://github.com/redis/redis-om-node/blob/main/docs/classes/Circle.md) that has been initialized with default values. We override those values by calling various builder methods to define the origin of our search (i.e. the longitude and latitude), the radius, and the units that radius is measured in. Valid units are `miles`, `meters`, `feet`, and `kilometers`.

Let's try the route out. I know we can find Joan Jett at around longitude -75.0 and latitude 40.0, which is in eastern Pennsylvania. So use those coordinates with a radius of 20 miles. You should receive in response:

[
  {
    "entityId": "01FYC7CTPKYNXQ98JSTBC37AS1",
    "firstName": "Joan",
    "lastName": "Jett",
    "age": 63,
    "verified": false,
    "location": {
      "longitude": -75.273,
      "latitude": 40.003
    },
    "locationUpdated": "2022-01-01T12:00:00.000Z",
    "skills": [
      "singing",
      "guitar",
      "black eyeliner"
    ],
    "personalStatement": "I love rock n' roll so put another dime in the jukebox, baby."
  }
]

Try widening the radius and see who else you can find.

title: Adding location tracking

text:
We're getting toward the end of the tutorial here, but before we go, I'd like to add that location tracking piece that I mentioned way back in the beginning. This next bit of code should be easily understood if you've gotten this far as it's not really doing anything I haven't talked about already.

Add a new file called `location-router.js` in the `routers` folder:

import { Router } from 'express'
import { personRepository } from '../om/person.js'

export const router = Router()

router.patch('/:id/location/:lng,:lat', async (req, res) => {

  const id = req.params.id
  const longitude = Number(req.params.lng)
  const latitude = Number(req.params.lat)

  const locationUpdated = new Date()

  const person = await personRepository.fetch(id)
  person.location = { longitude, latitude }
  person.locationUpdated = locationUpdated
  await personRepository.save(person)

  res.send({ id, locationUpdated, location: { longitude, latitude } })
})

Here we're calling `.fetch()` to fetch a person, we're updating some values for that person—the `.location` property with our longitude and latitude and the `.locationUpdated` property with the current date and time. Easy stuff.

To use this `Router`, import it in `server.js`:

/* import routers */
import { router as personRouter } from './routers/person-router.js'
import { router as searchRouter } from './routers/search-router.js'
import { router as locationRouter } from './routers/location-router.js'

And bind the router to a path:

/* bring in some routers */
app.use('/person', personRouter, locationRouter)
app.use('/persons', searchRouter)

And that's that. But this just isn't enough to satisfy. It doesn't show you anything new, except maybe the usage of a `date` field. And, it's not really location *tracking*. It just shows where these people last were, no history. So let's add some!.

To add some history, we're going to use a [Redis Stream](). Streams are a big topic but don't worry if you’re not familiar with them, you can think of them as being sort of like a log file stored in a Redis key where each entry represents an event. In our case, the event would be the person moving about or checking in or whatever.

But there's a problem. Redis OM doesn’t support Streams even though Redis Stack does. So how do we take advantage of them in our application? By using [Node Redis](https://github.com/redis/node-redis). Node Redis is a low-level Redis client for Node.js that gives you access to all the Redis commands and data types. Internally, Redis OM is creating and using a Node Redis connection. You can use that connection too. Or rather, Redis OM can be *told* to use the connection you are using. Let me show you how.

title: Using Node Redis

text:
Open up `client.js` in the `om` folder. Remember how we created a Redis OM `Client` and then called `.open()` on it?

const client = await new Client().open(url)

Well, the `Client` class also has a `.use()` method that takes a Node Redis connection. Modify `client.js` to open a connection to Redis using Node Redis and then `.use()` it:

import { Client } from 'redis-om'
import { createClient } from 'redis'

/* pulls the Redis URL from .env */
const url = process.env.REDIS_URL

/* create a connection to Redis with Node Redis */
export const connection = createClient({ url })
await connection.connect()

/* create a Client and bind it to the Node Redis connection */
const client = await new Client().use(connection)

export default client

And that's it. Redis OM is now using the `connection` you created. Note that we are exporting both the `client` *and* the `connection`. Got to export the `connection` if we want to use it in our newest route.

title: Storing location history with Streams

text:
To add an event to a Stream we need to use the [XADD]() command. Node Redis exposes that as `.xAdd()`. So, we need to add a call to `.xAdd()` in our route. Modify `location-router.js` to import our `connection`:

import { connection } from '../om/client.js'

And then in the route itself add a call to `.xAdd()`:

  ...snip...
  const person = await personRepository.fetch(id)
  person.location = { longitude, latitude }
  person.locationUpdated = locationUpdated
  await personRepository.save(person)

  let keyName = `${person.keyName}:locationHistory`
  await connection.xAdd(keyName, '*', person.location)
  ...snip...

`.xAdd()` takes a key name, an event ID, and a JavaScript object containing the keys and values that make up the event, i.e. the event data. For the key name, we're building a string using the `.keyName` property that `Person` inherited from `Entity` (which will return something like `Person:01FYC7CTPKYNXQ98JSTBC37AS1`) combined with a hard-coded value. We're passing in `*` for our event ID, which tells Redis to just generate it based on the current time and previous event ID. And we're passing in the location—with properties of longitude and latitude—as our event data.

Now, whenever this route is exercised, the longitude and latitude will be logged and the event ID will encode the time. Go ahead and use Swagger to move Joan Jett around a few times.

Now, go into Redis Insight and take a look at the Stream. You'll see it there in the list of keys but if you click on it, you'll get a message saying that "This data type is coming soon!". If you don't get this message, congratulations, you live in the future! For us here in the past, we'll just issue the raw command instead:

    XRANGE Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory - +

This tells Redis to get a range of values from a Stream stored in the given the key name—`Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory` in our example. The next values are the starting event ID and the ending event ID. `-` is the beginning of the Stream. `+` is the end. So this returns everything in the Stream:

    1) 1) "1647536562911-0"
      2) 1) "longitude"
          2) "45.678"
          3) "latitude"
          4) "45.678"
    2) 1) "1647536564189-0"
      2) 1) "longitude"
          2) "45.679"
          3) "latitude"
          4) "45.679"
    3) 1) "1647536565278-0"
      2) 1) "longitude"
          2) "45.680"
          3) "latitude"
          4) "45.680"

And just like that, we're tracking Joan Jett.

title: Wrap-up

text:
So, now you know how to use Express + Redis OM to build an API backed by Redis Stack. And, you've got yourself some pretty decent started code in the process. Good deal! If you want to learn more, you can check out the [documentation](https://github.com/redis/redis-om-node) for Redis OM. It covers the full breadth of Redis OM's features.

And thanks for taking the time to work through this. I sincerely hope you found it useful. If you have any questions, the [Redis Discord server](https://discord.gg/redis) is by far the best place to get them answered. Join the server and ask away!


title: RedisOM for Python

summary: Learn how to build with Redis Stack and Python

page_type: content

title: Overview

role: overview

text:
[Redis OM Python](https://github.com/redis/redis-om-python) is a Redis client that provides high-level abstractions for managing document data in Redis. This tutorial shows you how to get up and running with Redis OM Python, Redis Stack, and the [Flask](https://flask.palletsprojects.com/) micro-framework.

We'd love to see what you build with Redis Stack and Redis OM.  [Join the Redis community on Discord](https://discord.gg/redis) to chat with us about all things Redis OM and Redis Stack.  Read more about Redis OM Python [our announcement blog post](https://redis.com/blog/introducing-redis-om-for-python/).

title: Overview

text:
This application, an API built with Flask and a simple domain model, demonstrates common data manipulation patterns using Redis OM.

Our entity is a Person, with the following JSON representation:

[code example]

We'll let Redis OM handle generation of unique IDs, which it does using [ULIDs](https://github.com/ulid/spec).  Redis OM will also handle creation of unique Redis key names for us, as well as saving and retrieving entities from JSON documents stored in a Redis Stack database.

title: Getting Started

role: setup

title: Requirements

role: content

text:
To run this application you'll need:

* [git](https://git-scm.com/download) - to clone the repo to your machine.
* [Python 3.9 or higher](https://www.python.org/downloads/).
* A [Redis Stack](https://redis.io) database, or Redis with the [Search and Query]() and [JSON]() features installed. We've provided a `docker-compose.yml` for this. You can also [sign up for a free 30Mb database with Redis Cloud](https://redis.com/try-free/?utm_source=redisio&utm_medium=referral&utm_campaign=2023-09-try_free&utm_content=cu-redis_cloud_users) - be sure to check the Redis Stack option when creating your cloud database.
* [curl](https://curl.se/), or [Postman](https://www.postman.com/) - to send HTTP requests to the application.  We'll provide examples using curl in this document.
* Optional: [Redis Insight](https://redis.com/redis-enterprise/redis-insight/), a free data visualization and database management tool for Redis.  When downloading Redis Insight, be sure to select version 2.x or use the version that comes with Redis Stack.

title: Get the Source Code

text:
Clone the repository from GitHub:

[code example]

title: Start a Redis Stack Database, or Configure your Redis Cloud Credentials

text:
Next, we'll get a Redis Stack database up and running.  If you're using Docker:

[code example]

If you're using Redis Cloud, you'll need the hostname, port number, and password for your database.  Use these to set the `REDIS_OM_URL` environment variable like this:

[code example]

(This step is not required when working with Docker as the Docker container runs Redis on `localhost` port `6379` with no password, which is the default connection that Redis OM uses.)

For example if your Redis Cloud database is at port `9139` on host `enterprise.redis.com` and your password is `5uper53cret` then you'd set `REDIS_OM_URL` as follows:

[code example]

title: Create a Python Virtual Environment and Install the Dependencies

text:
Create a Python virtual environment, and install the project dependencies which are [Flask](https://pypi.org/project/Flask/), [Requests](https://pypi.org/project/requests/) (used only in the data loader script) and [Redis OM](https://pypi.org/project/redis-om/):

[code example]

title: Start the Flask Application

text:
Let's start the Flask application in development mode, so that Flask will restart the server for you each time you save code changes in `app.py`:

[code example]

If all goes well, you should see output similar to this:

[code example]

You're now up and running, and ready to perform CRUD operations on data with Redis, Search and Query, JSON and Redis OM for Python! To make sure the server's running, point your browser at `http://127.0.0.1:5000/`, where you can expect to see the application's basic home page:

![screenshot](./images/python_server_running.png)

title: Load the Sample Data

text:
We've provided a small amount of sample data (it's in `data/people.json`.  The Python script `dataloader.py` loads each person into Redis by posting the data to the application's create a new person endpoint.  Run it like this:

[code example]

Make sure to take a copy of the output of the data loader, as your IDs will differ from those used in the tutorial.  To follow along, substitute your IDs for the ones shown above.  e.g. whenever we are working with Kareem Khan, change `01FX8RMR7T60ANQTS4P9NKPKX8` for the ID that your data loader assigned to Kareem in your Redis database.

title: Problems?

text:
If the Flask server fails to start, take a look at its output.  If you see log entries similar to this:

[code example]

then you need to start the Redis Docker container if using Docker, or set the `REDIS_OM_URL` environment variable if using Redis Cloud.

If you've set the `REDIS_OM_URL` environment variable, and the code errors with something like this on startup:

[code example]

then you'll need to check that you used the correct hostname, port, password and format when setting `REDIS_OM_URL`.

If the data loader fails to post the sample data into the application, make sure that the Flask application is running **before** running the data loader.

title: Create, Read, Update and Delete Data

text: Let's create and manipulate some instances of our data model in Redis.  Here we'll look at how to call the Flask API with curl (you could also use Postman), how the code works, and how the data's stored in Redis.

title: Building a Person Model with Redis OM

text:
Redis OM allows us to model entities using Python classes, and the [Pydantic](https://pypi.org/project/pydantic/) framework.  Our person model is contained in the file `person.py`.  Here's some notes about how it works:

* We declare a class `Person` which extends a Redis OM class `JsonModel`.  This tells Redis OM that we want to store these entities in Redis as JSON documents.
* We then declare each field in our model, specifying the data type and whether or not we want to index on that field.  For example, here's the `age` field, which we've declared as a positive integer that we want to index on:

[code example]

* The `skills` field is a list of strings, declared thus:

[code example]

* For the `personal_statement` field, we don't want to index on the field's value, as it's a free text sentence rather than a single word or digit. For this, we'll tell Redis OM that we want to be able to perform full text searches on the values:

[code example]

* `address` works differently from the other fields.  Note that in our JSON representation of the model, address is an object rather than a string or numerical field.  With Redis OM, this is modeled as a second class, which extends the Redis OM `EmbeddedJsonModel` class:

[code example]

* Fields in an `EmbeddedJsonModel` are defined in the same way, so our class contains a field definition for each data item in the address.

* Not every field in our JSON is present in every address, Redis OM allows us to declare a field as optional so long as we don't index it:

[code example]

* We can also set a default value for a field... let's say country should be "United Kingdom" unless otherwise specified:

[code example]

* Finally, to add the embedded address object to our Person model, we declare a field of type `Address` in the Person class:

[code example]

title: Adding New People

text:
The function `create_person` in `app.py` handles the creation of a new person in Redis.  It expects a JSON object that adheres to our Person model's schema.  The code to then create a new Person object with that data and save it in Redis is simple:

[code example]

When a new Person instance is created, Redis OM assigns it a unique ULID primary key, which we can access as `.pk`.  We return that to the caller, so that they know the ID of the object they just created.

Persisting the object to Redis is then simply a matter of calling `.save()` on it.

Try it out... with the server running, add a new person using curl:

[code example]

Running the above curl command will return the unique ULID ID assigned to the newly created person. For example `01FX8SSSDN7PT9T3N0JZZA758G`.

title: Examining the data in Redis

text:
Let's take a look at what we just saved in Redis.  Using Redis Insight or redis-cli, connect to the database and look at the value stored at key `:person.Person:01FX8SSSDN7PT9T3N0JZZA758G`.  This is stored as a JSON document in Redis, so if using redis-cli you'll need the following command:

[code example]

If you're using Redis Insight, the browser will render the key value for you when you click on the key name:

![Data in Redis Insight](./images/python_insight_explore_person.png)

When storing data as JSON in Redis, we can update and retrieve the whole document, or just parts of it.  For example, to retrieve only the person's address and first skill, use the following command (Redis Insight users should use the built in redis-cli for this):

[code example]

For more information on the JSON Path syntax used to query JSON documents in Redis, see the [documentation]().

title: Find a Person by ID

text:
If we know a person's ID, we can retrieve their data.  The function `find_by_id` in `app.py` receives an ID as its parameter, and asks Redis OM to retrieve and populate a Person object using the ID and the Person `.get` class method:

[code example]

The `.dict()` method converts our Person object to a Python dictionary that Flask then returns to the caller.

Note that if there is no Person with the supplied ID in Redis, `get` will throw a `NotFoundError`.

Try this out with curl, substituting `01FX8SSSDN7PT9T3N0JZZA758G` for the ID of a person that you just created in your database:

[code example]

The server responds with a JSON object containing the user's data:

[code example]

title: Find People with Matching First and Last Name

text:
Let's find all the people who have a given first and last name...  This is handled by the function `find_by_name` in `app.py`.

Here, we're using Person's `find` class method that's provided by Redis OM.  We pass it a search query, specifying that we want to find people whose `first_name` field contains the value of the `first_name` parameter passed to `find_by_name` AND whose `last_name` field contains the value of the `last_name` parameter:

[code example]

`.all()` tells Redis OM that we want to retrieve all matching people.

Try this out with curl as follows:

[code example]

**Note:** First and last name are case sensitive.

The server responds with an object containing `results`, an array of matches:

[code example]

title: Find People within a Given Age Range

text:
It's useful to be able to find people that fall into a given age range... the function `find_in_age_range` in `app.py` handles this as follows...

We'll again use Person's `find` class method, this time passing it a minimum and maximum age, specifying that we want results where the `age` field is between those values only:

[code example]

Note that we can also use `.sort_by` to specify which field we want our results sorted by.

Let's find everyone between 30 and 47 years old, sorted by age:

[code example]

This returns a `results` object containing an array of matches:

[code example]

title: Find People in a Given City with a Specific Skill

text:
Now, we'll try a slightly different sort of query.  We want to find all of the people that live in a given city AND who also have a certain skill.  This requires a search over both the `city` field which is a string, and the `skills` field, which is an array of strings.

Essentially we want to say "Find me all the people whose city is `city` AND whose skills array CONTAINS `desired_skill`", where `city` and `desired_skill` are the parameters to the `find_matching_skill` function in `app.py`.  Here's the code for that:

[code example]

The `<<` operator here is used to indicate "in" or "contains".

Let's find all the guitar players in Sheffield:

[code example]

**Note:** `Sheffield` is case sensitive.

The server returns a `results` array containing matching people:

[code example]

title: Find People using Full Text Search on their Personal Statements

text:
Each person has a `personal_statement` field, which is a free text string containing a couple of sentences about them.  We chose to index this in a way that makes it full text searchable, so let's see how to use this now.  The code for this is in the function `find_matching_statements` in `app.py`.

To search for people who have the value of the parameter `search_term` in their `personal_statement` field, we use the `%` operator:

[code example]

Let's find everyone who talks about "play" in their personal statement.

[code example]

The server responds with a `results` array of matching people:

[code example]

Note that we get results including matches for "play", "plays" and "playing".

title: Update a Person's Age

text:
As well as retrieving information from Redis, we'll also want to update a Person's data from time to time.  Let's see how to do that with Redis OM for Python.

The function `update_age` in `app.py` accepts two parameters: `id` and `new_age`.  Using these, we first retrieve the person's data from Redis and create a new object with it:

[code example]

Assuming we find the person, let's update their age and save the data back to Redis:

[code example]

Let's change Kareem Khan's age from 27 to 28:

[code example]

The server responds with `ok`.

title: Delete a Person

text:
If we know a person's ID, we can delete them from Redis without first having to load their data into a Person object. In the function `delete_person` in `app.py`, we call the `delete` class method on the Person class to do this:

[code example]

Let's delete Dan Harris, the person with ID `01FX8RMR8545RWW4DYCE5MSZA1`:

[code example]

The server responds with an `ok` response regardless of whether the ID provided existed in Redis.

title: Setting an Expiry Time for a Person

role: configuration

text:
This is an example of how to run arbitrary Redis commands against instances of a model saved in Redis.  Let's see how we can set the time to live (TTL) on a person, so that Redis will expire the JSON document after a configurable number of seconds have passed.

The function `expire_by_id` in `app.py` handles this as follows.  It takes two parameters: `id` - the ID of a person to expire, and `seconds` - the number of seconds in the future to expire the person after.  This requires us to run the Redis [`EXPIRE`]() command against the person's key.  To do this, we need to access the Redis connection from the `Person` model like so:

[code example]

Let's set the person with ID `01FX8RMR82D091TC37B45RCWY3` to expire in 600 seconds:

[code example]

Using `redis-cli`, you can check that the person now has a TTL set with the Redis `expire` command:

[code example]

This shows that Redis will expire the key 584 seconds from now.

You can use the `.db()` function on your model class to get at the underlying redis-py connection whenever you want to run lower level Redis commands.  For more details, see the [redis-py documentation](https://redis.readthedocs.io/en/stable/).

title: Shutting Down Redis (Docker)

text:
If you're using Docker, and want to shut down the Redis container when you are finished with the application, use `docker-compose down`:

[code example]

first_name: A string, the person's first or given name

last_name: A string, the person's last or surname

unit: A string, optional unit number e.g. B or 1

street_name: A string, name of the street they live on

city: A string, name of the city they live in

state: A string, state, province or county that they live in

postal_code: A string, their zip or postal code

country: A string, country that they live in.

personal_statement: A string, free text personal statement

skills: A string: another still that the person has

section_id: overview

code:
$ git clone https://github.com/redis-developer/redis-om-python-flask-skeleton-app.git
$ cd redis-om-python-flask-skeleton-app

section_id: get-the-source-code

code:
$ docker-compose up -d
Creating network "redis-om-python-flask-skeleton-app_default" with the default driver
Creating redis_om_python_flask_starter ... done

code:
$ python3 -m venv venv
$ . ./venv/bin/activate
$ pip install -r requirements.txt

code:
$ export FLASK_ENV=development
$ flask run

section_id: start-the-flask-application

code:
$ flask run
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: XXX-XXX-XXX

code:
$ python dataloader.py
Created person Robert McDonald with ID 01FX8RMR7NRS45PBT3XP9KNAZH
Created person Kareem Khan with ID 01FX8RMR7T60ANQTS4P9NKPKX8
Created person Fernando Ortega with ID 01FX8RMR7YB283BPZ88HAG066P
Created person Noor Vasan with ID 01FX8RMR82D091TC37B45RCWY3
Created person Dan Harris with ID 01FX8RMR8545RWW4DYCE5MSZA1

section_id: load-the-sample-data

code:
raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 61 connecting to localhost:6379. Connection refused.

section_id: problems

code:
raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 8 connecting to enterprise.redis.com:9139. nodename nor servname provided, or not known.

code: age: PositiveInt = Field(index=True)

section_id: building-a-person-model-with-redis-om

code: skills: List[str] = Field(index=True)

code: personal_statement: str = Field(index=True, full_text_search=True)

code:
class Address(EmbeddedJsonModel):
    # field definitions...

code: unit: Optional[str] = Field(index=False)

code: country: str = Field(index=True, default="United Kingdom")

code: address: Address

code:
new_person = Person(**request.json)
  new_person.save()
  return new_person.pk

section_id: adding-new-people

code:
curl --location --request POST 'http://127.0.0.1:5000/person/new' \
--header 'Content-Type: application/json' \
--data-raw '{
    "first_name": "Joanne",
    "last_name": "Peel",
    "age": 36,
    "personal_statement": "Music is my life, I love gigging and playing with my band.",
    "address": {
      "street_number": 56,
      "unit": "4A",
      "street_name": "The Rushes",
      "city": "Birmingham",
      "state": "West Midlands",
      "postal_code": "B91 6HG",
      "country": "United Kingdom"
    },
    "skills": [
      "synths",
      "vocals",
      "guitar"
    ]
}'

section_id: examining-the-data-in-redis

code:
$ redis-cli
127.0.0.1:6379> json.get :person.Person:01FX8SSSDN7PT9T3N0JZZA758G $.address $.skills[0]
"{\"$.skills[0]\":[\"synths\"],\"$.address\":[{\"pk\":\"01FX8SSSDNRDSRB3HMVH00NQTT\",\"street_number\":56,\"unit\":\"4A\",\"street_name\":\"The Rushes\",\"city\":\"Birmingham\",\"state\":\"West Midlands\",\"postal_code\":\"B91 6HG\",\"country\":\"United Kingdom\"}]}"

code:
try:
      person = Person.get(id)
      return person.dict()
  except NotFoundError:
      return {}

section_id: find-a-person-by-id

code: curl --location --request GET 'http://localhost:5000/person/byid/01FX8SSSDN7PT9T3N0JZZA758G'

city: Birmingham

country: United Kingdom

postal_code: B91 6HG

state: West Midlands

street_name: The Rushes

first_name: Joanne

last_name: Peel

skills: synths vocals guitar

code:
people = Person.find(
      (Person.first_name == first_name) &
      (Person.last_name == last_name)
  ).all()

code: curl --location --request GET 'http://127.0.0.1:5000/people/byname/Kareem/Khan'

city: Sheffield

postal_code: S1 5RE

state: South Yorkshire

street_name: The Beltway

unit: A

first_name: Kareem

last_name: Khan

personal_statement: I'm Kareem, a multi-instrumentalist and singer looking to join a new rock band.

skills: drums guitar synths

code:
people = Person.find(
      (Person.age >= min_age) &
      (Person.age <= max_age)
  ).sort_by("age").all()

section_id: find-people-within-a-given-age-range

code: curl --location --request GET 'http://127.0.0.1:5000/people/byage/30/47'

postal_code: S12 2MX

street_name: Main Street

first_name: Robert

last_name: McDonald

personal_statement: My name is Robert, I love meeting new people and enjoy music, coding and walking my dog.

skills: guitar piano trombone

city: Nottingham

postal_code: NG1 1AA

state: Nottinghamshire

street_name: Broadway

first_name: Noor

last_name: Vasan

personal_statement: I sing and play the guitar, I enjoy touring and meeting new people on the road.

skills: vocals guitar

city: San Diego

country: United States

state: California

street_name: C Street

personal_statement: I'm in a really cool band that plays a lot of cover songs.  I'm the drummer!

code:
people = Person.find(
      (Person.skills << desired_skill) &
      (Person.address.city == city)
  ).all()

code: curl --location --request GET 'http://127.0.0.1:5000/people/byskill/guitar/Sheffield'

code: curl --location --request GET 'http://127.0.0.1:5000/people/bystatement/play'

code:
try:
      person = Person.get(id)

  except NotFoundError:
      return "Bad request", 400

code: curl --location --request POST 'http://127.0.0.1:5000/person/01FX8RMR7T60ANQTS4P9NKPKX8/age/28'

code: curl --location --request POST 'http://127.0.0.1:5000/person/01FX8RMR8545RWW4DYCE5MSZA1/delete'

code:
person_to_expire = Person.get(id)
  Person.db().expire(person_to_expire.key(), seconds)

code: curl --location --request POST 'http://localhost:5000/person/01FX8RMR82D091TC37B45RCWY3/expire/600'

code:
$ docker-compose down
Stopping redis_om_python_flask_starter ... done
Removing redis_om_python_flask_starter ... done
Removing network redis-om-python-flask-skeleton-app_default


title: RedisVL

summary: This is the Redis vector library (RedisVL).

title: Overview

text: RedisVL provides a powerful, dedicated Python client library for using Redis as a vector database. Leverage Redis's speed, reliability, and vector-based semantic search capabilities to supercharge your application.

title: Overview

text: RedisVL (Redis Vector Library) is a Python client library specifically designed for building AI applications with Redis as a vector database. It provides high-level abstractions for vector search, semantic caching, and AI-powered applications while leveraging Redis's performance and reliability.

title: Key Features

text:
- **Vector Search**: High-performance similarity search with multiple distance metrics
- **Semantic Caching**: Intelligent caching for AI model responses and embeddings
- **Schema Management**: Declarative schema definition for vector indexes
- **Multiple Vectorizers**: Built-in support for OpenAI, Hugging Face, and custom embeddings
- **Query Filtering**: Advanced filtering capabilities for precise search results
- **Real-time Updates**: Live vector index updates and real-time search
- **Python Integration**: Native Python API with pandas and NumPy compatibility
- **Production Ready**: Enterprise-grade performance and reliability with Redis

title: Getting Started

text: Refer to the complete [RedisVL documentation]() for installation, setup, and usage examples.


title: Documentation

summary: RIOT documentation

title: Content

text: Full documentation for RIOT is available at [redis.github.io/riot](https://redis.github.io/riot/).


title: RIOT

summary: Redis Input/Output Tools

summary: Install RIOT on macOS, Linux, Windows, and Docker

title: Install

summary: RIOT quick start guide

title: Quick Start

summary: RIOT documentation

title: Documentation

title: Content

text:
Redis Input/Output Tools (RIOT) is a command-line utility designed to help you get data in and out of Redis.

It supports many different sources and targets:

* [Files](https://redis.github.io/riot/#_file) (CSV, JSON, XML)
* [Data generators](https://redis.github.io/riot/#_datagen) (Redis data structures, Faker)
* [Relational databases](https://redis.github.io/riot/#_db)
* [Redis itself](https://redis.github.io/riot/#_replication) (snapshot and live replication)

Full documentation is available at [redis.github.io/riot](https://redis.github.io/riot/)


title: Install

title: Overview

text: RIOT can be installed in different ways depending on your environment and preference.

title: macOS via Homebrew

text: [code example]

title: Windows via Scoop

text: [code example]

title: Linux via Homebrew

text: [code example]

title: Docker

text: [code example]

title: Manual installation

text:
Download the pre-compiled binary from the [releases page](https://github.com/redis/riot/releases), uncompress, and copy to the desired location.

Full documentation is available at [redis.github.io/riot](https://redis.github.io/riot/).

code: brew install redis/tap/riot

section_id: macos-via-homebrew

code:
scoop bucket add redis https://github.com/redis/scoop.git
scoop install riot

section_id: windows-via-scoop

section_id: linux-via-homebrew

code: docker run riotx/riot [OPTIONS] [COMMAND]

section_id: docker


title: Quick Start

summary: RIOT quick start guide

title: Content

text:
You can launch RIOT with the following command:

[code example]

This will show usage help, which you can also get by running:

[code example]

You can use `--help` on any command and sub-command:

[code example]

Full documentation is available at [redis.github.io/riot](https://redis.github.io/riot/).

code: riot

section_id: content

code: riot --help

code:
riot command --help
riot command sub-command --help


title: Rust client for Redis

summary: Learn how to build with Redis and Rust

title: Overview

text: Connect your Rust application to a Redis database using the redis-rs client library.

title: Overview

text: redis-rs is a high-performance Redis client for Rust that leverages Rust's memory safety and performance characteristics. It provides both synchronous and asynchronous APIs with excellent integration into the Rust ecosystem.

title: Key Features

text:
- **Memory Safety**: Rust's ownership system prevents memory leaks and data races
- **High Performance**: Zero-cost abstractions and efficient memory usage
- **Async Support**: Tokio integration for asynchronous, non-blocking operations
- **Type Safety**: Strong typing with compile-time error checking
- **Connection Pooling**: Efficient connection management for concurrent applications
- **Pipeline Support**: Command batching for improved throughput
- **Cluster Support**: Redis Cluster client for distributed setups
- **Comprehensive API**: Full Redis command coverage with idiomatic Rust interfaces

title: Getting Started

text: Refer to the [Rust Redis documentation](https://docs.rs/redis/latest/redis/) to install, connect, and use redis-rs.


title: Spring Data Redis

summary: Plug Redis into your Spring application with minimal effort

title: Overview

text:
Spring Data Redis implements the Spring framework's cache abstraction for Redis, which allows you to plug Redis into your Spring application with minimal effort.

Spring's cache abstraction applies cache-aside to methods, reducing executions by storing and reusing results. When a method is invoked, the abstraction checks if it's been called with the same arguments before. If so, it returns the cached result. If not, it invokes the method, caches the result, and returns it. This way, costly methods are invoked less often. Further details are in the [Spring cache abstraction documentation](https://docs.spring.io/spring-framework/reference/integration/cache.html).

title: Get started

text:
In a nutshell, you need to perform the following steps to use Redis as your cache storage:

1. [Configure the cache storage](https://docs.spring.io/spring-framework/reference/integration/cache/store-configuration.html) by using the [Redis cache manager](https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html) that is part of Spring Data.
2. Annotate a repository with your `@CacheConfig`.
3. Use the `@Cachable` annotation on a repository method to cache the results of that method.

Here is an example:

[code example]

title: Further readings

text:
Please read the Spring framework's documentation to learn more about how to use the Redis cache abstraction for Spring:

* [Spring cache abstraction](https://docs.spring.io/spring-framework/reference/integration/cache.html)
* [Spring cache store configuration](https://docs.spring.io/spring-framework/reference/integration/cache/store-configuration.html)
* [Spring Data Redis Cache](https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html)

code:
@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {

    @Cacheable
    public Book findBook(ISBN isbn) {...}
}

section_id: get-started


title: Get started with Terraform

summary: Shows how to install the Redis Cloud provider and create a subscription.

title: Overview

text: Here, you'll learn how to use the [Redis Cloud Terraform Provider]() to create a subscription and a database.

title: Prerequisites

text:
1. [Install Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli).

1. [Create a Redis Cloud account]() if you do not have one already.

1. [Enable the Redis Cloud API]().

1. Get your Redis Cloud [API keys](). Set them to the following environment variables:

    - Set `REDISCLOUD_ACCESS_KEY` to your API account key.
    - Set `REDISCLOUD_SECRET_KEY` to your API user key.

1. Set a [payment method]().

title: Install the Redis Cloud provider

text:
1. Create a file to contain the Terraform configuration called `main.tf`.

1. Go to the [Redis Cloud Terraform Registry](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/).

1. Select **Use Provider** and copy the Terraform code located there. Paste the code into `main.tf` and save the file.

   [code example]

1. Run `terraform init`.

title: Create a Redis Cloud subscription with Terraform

text:
In your Terraform configuration file, you can add resources and data sources to plan and create subscriptions and databases. See the [Redis Cloud Terraform Registry documentation](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs) for more info about the resources and data sources you can use as part of the Redis Cloud provider.

The steps in this section show you how to plan and create a Redis Cloud Pro subscription with one database.

1. Use the [`rediscloud_payment_method`](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_payment_method) data source to get the payment method ID.

    [code example]

   Example:

   [code example]

1. Define a [`rediscloud_subscription`](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_subscription) resource to create the subscription.

    [code example]

   Example:

   [code example]

1. Define a [`rediscloud_subscription_database`](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_subscription_database) resource to create a database.

    [code example]

   Example:

   [code example]

2. Run `terraform plan` to check for any syntax errors.

    [code example]

3. Run `terraform apply` to apply the changes and enter `yes` to confirm when prompted.

    This will take some time. You will see messages in your terminal while the subscription and database are being created:

   [code example]

   When provisioning is complete, you will see a message in your terminal:

   [code example]

   View the [Redis Cloud console](https://cloud.redis.io/) to verify your subscription and database creation.

4. If you want to remove these sample resources, run `terraform destroy`.

title: More info

text:
- [Redis Cloud Terraform Registry](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs)
- [Terraform documentation](https://developer.hashicorp.com/terraform/docs)
- [Terraform configuration syntax](https://developer.hashicorp.com/terraform/language/syntax/configuration)

code:
provider "rediscloud" {
   }

   # Example resource configuration
   resource "rediscloud_subscription" "example" {
      # ...
   }

section_id: install-the-redis-cloud-provider

code:
# Get credit card details
    data "rediscloud_payment_method" "card" {
        card_type = "<Card type>"
        last_four_numbers = "<Last four numbers on the card>"
    }

code:
data "rediscloud_payment_method" "card" {
      card_type = "Visa"
      last_four_numbers = "5625"
   }

code:
# Create a subscription
    resource "rediscloud_subscription" "subscription-resource" {
            name = "subscription-name"
            payment_method_id = data.rediscloud_payment_method.card.id # If you want to pay with a marketplace account, replace this line with payment_method = 'marketplace'.
            memory_storage = "ram"

            # Specify the cloud provider information here
            cloud_provider {
                    provider = "<Cloud provider>"
                    region {
                            region = "<region>"
                            networking_deployment_cidr = "<CIDR>"
                    }
            }

            #Define the average database specification for databases in the subscription
            creation_plan {
                    memory_limit_in_gb = 2
                    quantity = 1
                    replication = true
                    throughput_measurement_by = "operations-per-second"
                    throughput_measurement_value = 20000
            }
    }

code:
resource "rediscloud_subscription" "subscription-resource" {
        name = "redis-docs-sub"
        payment_method_id = data.rediscloud_payment_method.card.id # If you want to pay with a marketplace account, replace this line with payment_method = 'marketplace'.
        memory_storage = "ram"

        cloud_provider {
                provider = "GCP"
                region {
                        region = "us-west1"
                        networking_deployment_cidr = "192.168.0.0/24"
                }
        }

        creation_plan {
                memory_limit_in_gb = 2
                quantity = 1
                replication = true
                throughput_measurement_by = "operations-per-second"
                throughput_measurement_value = 20000
                modules = ["RedisJSON"]
        }
   }

code:
# Create a Database
    resource "rediscloud_subscription_database" "database-resource" {
        subscription_id = rediscloud_subscription.subscription-resource.id
        name = "database-name"
        memory_limit_in_gb = 2
        data_persistence = "aof-every-write"
        throughput_measurement_by = "operations-per-second"
        throughput_measurement_value = 20000
        replication = true

        alert {
        name = "dataset-size"
        value = 40
        }
        depends_on = [rediscloud_subscription.subscription-resource]

    }

code:
resource "rediscloud_subscription_database" "database-resource" {
       subscription_id = rediscloud_subscription.subscription-resource.id
       name = "redis-docs-db"
       memory_limit_in_gb = 2
       data_persistence = "aof-every-write"
       throughput_measurement_by = "operations-per-second"
       throughput_measurement_value = 20000
       replication = true

       modules = [
       {
            name = "RedisJSON"
       }
       ]

       alert {
       name = "dataset-size"
       value = 40
       }
      depends_on = [rediscloud_subscription.subscription-resource]

   }

code:
$ terraform plan
    data.rediscloud_payment_method.card: Reading...
    data.rediscloud_payment_method.card: Read complete after 1s [id=8859]

    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
    symbols:
    + create

    Terraform will perform the following actions:

        # rediscloud_subscription.subscription-resource will be created
        + resource "rediscloud_subscription" "subscription-resource" {
            [...]
        }

        # rediscloud_subscription_database.database-resource will be created
        + resource "rediscloud_subscription_database" "database-resource" {
            [...]
        }

    Plan: 2 to add, 0 to change, 0 to destroy.

code:
rediscloud_subscription.subscription-resource: Creating...
   rediscloud_subscription.subscription-resource: Still creating... [10s elapsed]
   rediscloud_subscription.subscription-resource: Still creating... [20s elapsed]
   rediscloud_subscription.subscription-resource: Still creating... [30s elapsed]

code: Apply complete! Resources: 2 added, 0 changed, 0 destroyed.


title: Terraform provider for Redis Cloud

title: Get started with Terraform

title: Overview

text:
[Terraform](https://developer.hashicorp.com/terraform) is an open source automation tool developed by Hashicorp that allows you to easily provision infrastructure as code.

Redis develops and maintains a [Terraform provider for Redis Cloud](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest). The Redis Cloud Terraform provider allows many of the same actions as found in the [Redis Cloud API]().

See [Get started with Terraform]() for an example of how to use the Terraform provider.

title: Data sources and Resources

text:
The Terraform provider represents API actions as data sources and resources. Data sources are read-only and allow you to get information, while resources allow you to create and manage infrastructure.

The Redis Cloud Terraform provider allows for the following data sources:

- Redis Cloud Pro:
  - [Subscriptions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_subscription)
  - [Databases](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_database)
  - [Database capabilities](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_database_modules)
  - [VPC peering connections](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_subscription_peerings)
  - [Cloud accounts](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_cloud_account)
  - [Supported persistence options](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_data_persistence)
  - [AWS Transit Gateways](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_transit_gateway)
  - Google Cloud Private Service Connect [Services](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_private_service_connect) and [Endpoints](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_private_service_connect_endpoints)
- Redis Cloud Essentials:
  - [Plans](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_essentials_plan)
  - [Subscriptions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_essentials_subscription)
  - [Databases](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_essentials_database)
- Active-Active:
  - [Subscriptions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_subscription)
  - [Databases](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_subscription_database)
  - [AWS Transit Gateways](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_active_active_transit_gateway)
  - Google Cloud Private Service Connect [services](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_active_active_private_service_connect) and [endpoints](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_active_active_private_service_connect_endpoints)
- [Payment methods](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_payment_method)
- [Supported cloud provider regions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_regions)
- ACL [roles](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_acl_role), [rules](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_acl_rule), and [users](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/data-sources/rediscloud_acl_user)

It also allows you to create and manage the following resources:

- Redis Cloud Pro:
  - [Subscriptions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_subscription)
  - [Databases](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_subscription_database)
    - **NOTE**: Upgrade your Terraform provider to version 1.8.1 to create databases with Search and Query.
  - [VPC peering connections](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_subscription_peering)
  - [Cloud accounts](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_cloud_account)
  - [AWS Transit Gateway attachments](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_transit_gateway_attachment)
  - Google Cloud Private Service Connect [connections](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_private_service_connect), [endpoints](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_private_service_connect_endpoint) and [endpoint acceptors](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_private_service_connect_endpoint_accepter)
- Redis Cloud Essentials:
  - [Subscriptions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_essentials_subscription)
  - [Databases](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_essentials_database)
- Active-Active:
  - [Subscriptions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_subscription)
  - [Databases](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_subscription_database)
  - [Regions](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_subscription_regions)
  - [VPC peering connections](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_subscription_peering)
  - [AWS Transit Gateway attachments](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_transit_gateway_attachment)
  - Google Cloud Private Service Connect [connections](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_private_service_connect), [endpoints](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_private_service_connect_endpoint) and [endpoint acceptors](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_active_active_private_service_connect_endpoint_accepter)
- ACL [rules](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_acl_rule), [roles](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_acl_role), and [users](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs/resources/rediscloud_acl_user)

title: More info

text:
- [Get started with Terraform]()
- [Redis Cloud Terraform Registry](https://registry.terraform.io/providers/RedisLabs/rediscloud/latest/docs)
- [Terraform documentation](https://developer.hashicorp.com/terraform/docs)
- [Terraform configuration syntax](https://developer.hashicorp.com/terraform/language/syntax/configuration)


title: Uptrace with Redis Software

summary: To collect, view, and monitor metrics data from your databases and other cluster components, you can connect Uptrace to your Redis Software cluster using OpenTelemetry Collector.

title: Overview

text:
Uptrace is an [open source APM tool](https://uptrace.dev/get/open-source-apm.html) that supports distributed tracing, metrics, and logs. You can use it to monitor applications and set up automatic alerts to receive notifications.

Uptrace uses OpenTelemetry to collect and export telemetry data from software applications such as Redis. OpenTelemetry is an open source observability framework that aims to provide a single standard for all types of observability signals such as traces, metrics, and logs.

With OpenTelemetry Collector, you can receive, process, and export telemetry data to any [OpenTelemetry backend](https://uptrace.dev/blog/opentelemetry-backend.html). You can also use Collector to scrape Prometheus metrics provided by Redis and then export those metrics to Uptrace.

You can use Uptrace to:

- Collect and display data metrics not available in the [admin console]().
- Use prebuilt dashboard templates maintained by the Uptrace community.
- Set up automatic alerts and receive notifications via email, Slack, Telegram, and others.
- Monitor your app performance and logs using [OpenTelemetry tracing](https://uptrace.dev/opentelemetry/distributed-tracing.html).

title: Install Collector and Uptrace

text:
Because installing OpenTelemetry Collector and Uptrace can take some time, you can use the [docker-compose](https://github.com/uptrace/uptrace/tree/master/example/redis-enterprise) example that also comes with Redis Software cluster.

After you download the Docker example, you can edit the following configuration files in the `uptrace/example/redis-enterprise` directory before you start the Docker containers:

- `otel-collector.yaml` - Configures `/etc/otelcol-contrib/config.yaml` in the OpenTelemetry Collector container.
- `uptrace.yml` - Configures`/etc/uptrace/uptrace.yml` in the Uptrace container.

You can also install OpenTelemetry and Uptrace from scratch using the following guides:

- [Getting started with OpenTelemetry Collector](https://uptrace.dev/opentelemetry/collector.html)
- [Getting started with Uptrace](https://uptrace.dev/get/get-started.html)

After you install Uptrace, you can access the Uptrace UI at [http://localhost:14318/](http://localhost:14318/).

title: Scrape Prometheus metrics

text:
Redis Software cluster exposes a Prometheus scraping endpoint on `http://localhost:8070/`. You can scrape that endpoint by adding the following lines to the OpenTelemetry Collector config:

[code example]

Next, you can export the collected metrics to Uptrace using OpenTelemetry protocol (OTLP):

[code example]

Don't forget to restart the Collector and then check logs for any errors:

[code example]

You can also check the full OpenTelemetry Collector config [here](https://github.com/uptrace/uptrace/blob/master/example/redis-enterprise/otel-collector.yaml).

title: View metrics

text:
When metrics start arriving to Uptrace, you should see a couple of dashboards in the Metrics tab. In total, Uptrace should create 3 dashboards for Redis Software metrics:

- "Redis: Nodes" dashboard displays a list of cluster nodes. You can select a node to view its metrics.

- "Redis: Databases" displays a list of Redis databases in all cluster nodes. To find a specific database, you can use filters or sort the table by columns.

- "Redis: Shards" contains a list of shards that you have in all cluster nodes. You can filter or sort shards and select a shard for more details.

title: Monitor metrics

text:
To start monitoring metrics, you need to create metrics monitors using Uptrace UI:

- Open "Alerts" -> "Monitors".
- Click "Create monitor" -> "Create metrics monitor".

For example, the following monitor uses the `group by node` expression to create an alert whenever an individual Redis shard is down:

[code example]

You can also create queries with more complex expressions.

For example, the following monitors create an alert when the keyspace hit rate is lower than 75% or memory fragmentation is too high:

[code example]

You can learn more about the query language [here](https://uptrace.dev/get/querying-metrics.html).

code:
# /etc/otelcol-contrib/config.yaml

prometheus_simple/cluster1:
  collection_interval: 10s
  endpoint: "localhost:8070" # Redis Cluster endpoint
  metrics_path: "/"
  tls:
    insecure: false
    insecure_skip_verify: true
    min_version: "1.0"

section_id: scrape-prometheus-metrics

code:
# /etc/otelcol-contrib/config.yaml

receivers:
  otlp:
    protocols:
      grpc:
      http:

exporters:
  otlp/uptrace:
    # Uptrace is accepting metrics on this port
    endpoint: localhost:14317
    headers: { "uptrace-dsn": "http://project1_secret_token@localhost:14317/1" }
    tls: { insecure: true }

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/uptrace]
    metrics:
      receivers: [otlp, prometheus_simple/cluster1]
      processors: [batch]
      exporters: [otlp/uptrace]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/uptrace]

code:
docker-compose logs otel-collector

# or

sudo journalctl -u otelcol-contrib -f

code:
monitors:
  - name: Redis shard is down
    metrics:
      - redis_up as $redis_up
    query:
      - group by cluster # monitor each cluster,
      - group by bdb # each database,
      - group by node # and each shard
      - $redis_up
    min_allowed_value: 1
    # shard should be down for 5 minutes to trigger an alert
    for_duration: 5m

section_id: monitor-metrics

code:
monitors:
  - name: Redis read hit rate < 75%
    metrics:
      - redis_keyspace_read_hits as $hits
      - redis_keyspace_read_misses as $misses
    query:
      - group by cluster
      - group by bdb
      - group by node
      - $hits / ($hits + $misses) as hit_rate
    min_allowed_value: 0.75
    for_duration: 5m

  - name: Memory fragmentation is too high
    metrics:
      - redis_used_memory as $mem_used
      - redis_mem_fragmentation_ratio as $fragmentation
    query:
      - group by cluster
      - group by bdb
      - group by node
      - where $mem_used > 32mb
      - $fragmentation
    max_allowed_value: 3
    for_duration: 5m


title: Redis with Vercel

summary: Integrate Redis with Vercel for fast, scalable web applications

title: Overview

text: Integrate Redis with your Vercel applications to add fast caching, session management, and real-time features to your web applications.

title: Overview

text: Redis integration with Vercel enables you to add high-performance data storage and caching to your serverless applications. Perfect for Next.js applications deployed on Vercel, Redis provides fast data access and real-time capabilities.

title: Key Features

text:
- **Serverless Optimization**: Optimized for Vercel's serverless environment
- **Edge Caching**: Global data caching for improved performance
- **Session Management**: Persistent session storage across serverless functions
- **Real-time Features**: WebSocket support for live updates and notifications
- **API Acceleration**: Cache API responses for faster load times
- **Environment Integration**: Seamless integration with Vercel environment variables
- **Auto-scaling**: Scales automatically with your Vercel application traffic
- **Global Distribution**: Redis databases close to Vercel's edge locations

title: Getting Started

text: Learn how to [integrate Redis with Vercel]() for optimal performance.


title: Write-behind architecture

summary: Discover the main components of Write-behind

title: Overview

text:
Write-behind lets you integrate Redis Enterprise (as the source of changes to data) and downstream databases or datastores.
Write-behind captures any changes to a selected set of key patterns in a Redis keyspace and asynchronously writes them in small batches to the downstream database. This means that your app doesn't need to handle the data remodeling or manage the connection with the downstream database.

Write-behind can normalize a key in Redis to several records in one or more tables at the target.
To learn more about write-behind declarative jobs and normalization, see the
[write-behind quick start guide]().

title: Write-behind topology

text:
Write-behind's CLI and engine are shipped as one product that can run both ingest and write-behind pipelines.
However, the two different types of pipeline have different topologies.

The Write-behind engine is installed on the Redis database that contains the application data and not on a separate staging Redis database. The Write-behind data streams and its control plane add only a small footprint of a few hundred MB to the Redis database. In the write-behind topology, Write-behind processes data in parallel on each shard and establishes a single connection from each shard to the downstream database.

title: Model translation

text:
Write-behind can track changes to the following Redis types:

- [Hash]()
- [JSON]()
- [Set]()
- [Sorted Set]()

Unlike the ingest scenario, write-behind has no default behavior for model translation. You must always
create a declarative job to specify the mapping between Redis keys and target database records.
The job configuration has `keys` and `mapping` sections that help make this an easy task.

title: Write-behind components

title: Write-behind CLI

text:
Write-behind's Python-based CLI is highly intuitive to use and performs validations to help you avoid mistakes.
The CLI makes it easy to set up Write-behind and manage it over its entire lifecycle.

title: Write-behind Engine

text:
Write-behind uses Redis Gears as its runtime environment. The Gears and Write-behind engine logic are installed
on all source Redis Enterprise database shards, but only primary shards process events and handle the pipeline.

The Write-behind Engine reads Redis change events from Redis Streams (one for each tracked key-pattern),
processes them, and translates them to SQL or whatever other language the target database uses.

Write-behind writes changes to the target in small batches using transactions. Write-behind guarantees
*at-least once* delivery. If any network problems, disconnections, or other temporary failures occur,
Write-behind will keep attempting to write the changes to the target. If a hard reject occurs, Write-behind keeps the reject
record and the reason in a *dead letter queue (DLQ)*.

title: Write-behind configuration

text:
The Write-behind configuration is persisted at the cluster level. The configuration is written by the CLI
[`deploy`]()
command, which saves all changes to the configuration file. This mechanism allows for automatic configuration of new shards
whenever you need them, and it can survive shard and node failure.