# Node.js 的 Redis OM

了解如何使用 Redis Stack 和 Node.js 进行构建

本教程将向您展示如何使用 Node.js 和 Redis Stack 构建 API。

我们将使用 ExpressRedis OM 来执行此操作,并且我们假设您对 Express 有基本的了解。

我们将要构建的 API 是一个简单且相对 RESTful API,它可以读取、写入和查找有关人员的数据:名字、姓氏、年龄等。我们还将添加一个简单的位置跟踪功能,只是为了额外的兴趣。

但在开始编码之前,让我们先描述一下 Redis OM什么。

# Node.js 的 Redis OM

Redis OM(读作 REDiss OHM)是一个为 Redis 提供对象映射的库——这就是 OM 的意思……对象映射。它将 Redis 数据类型(特别是哈希和 JSON 文档)映射到 JavaScript 对象。它允许您搜索这些哈希和 JSON 文档。它使用 RedisJSON 和 RediSearch 来做到这一点。

RedisJSON 和 RediSearch 是 Redis Stack 中包含的两个模块。模块是添加新数据类型和新命令的 Redis 扩展。RedisJSON 添加了 JSON 文档数据类型和操作它的命令。RediSearch 添加了各种搜索命令来索引 JSON 文档和哈希的内容。

Redis OM 有四个不同的版本。在本教程中,我们将使用适用于 Node.js 的 Redis OM,但也有适用于 Python.NETSpring 的风格和教程。

本教程将帮助您开始使用 Redis OM for Node.js,涵盖基础知识。但是,如果您想深入了解Redis OM 的所有功能,请查看GitHub 上 的 README 。

# 先决条件

像任何与软件相关的东西一样,您需要先安装一些依赖项才能开始:

  • Node.js 14.8+ await :在本教程中,我们使用的是 Node 14.8 中引入的JavaScript 顶级特性。因此,请确保您使用的是该版本或更高版本。
  • Redis Stack :您需要一个 Redis Stack 版本,可以在您的机器上本地运行,也可以 在云 中运行。
  • RedisInsight :我们将使用它来查看 Redis 内部,并确保我们的代码正在做我们认为它正在做的事情。

# 入门代码

我们不会完全从头开始编写代码。相反,我们为您提供了一些入门代码。继续并将其克隆到您方便的文件夹中:

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

现在您已经有了起始代码,让我们稍微探索一下。在根目录中打开,server.js我们看到我们有一个简单的 Express 应用程序,它使用 *Dotenv* 进行配置,使用 Swagger UI Express 测试我们的 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)

除此之外api.yaml,它定义了我们将要构建的 API,并提供了 Swagger UI Express 呈现其 UI 所需的信息。除非你想添加一些额外的路线,否则你不需要弄乱它。

persons文件夹有一些 JSON 文件和一个 shell 脚本。JSON 文件是样本人员(所有音乐家都是因为有趣),您可以将其加载到 API 中进行测试。shell 脚本load-data.sh—— 将使用curl.

有两个空文件夹omrouters. 该om文件夹是所有 Redis OM 代码所在的位置。该routers文件夹将保存我们所有 Express 路线的代码。

# 配置并运行

如果有点薄,启动代码完全可以运行。在我们继续编写实际代码之前,让我们配置并运行它以确保它可以正常工作。首先,获取所有依赖项:

npm install

然后,在根目录中设置一个.envDotenv 可以使用的文件。根目录中有一个sample.env文件,您可以复制和修改:

cp sample.env .env

的内容.env如下所示:

# 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

这很有可能已经是正确的。但是,如果您需要REDIS_URL针对您的特定环境(例如,您在云中运行 Redis Stack)进行更改,那么现在就是这样做的时候了。完成后,您应该能够运行该应用程序:

npm start

导航到http://localhost:8080并检查 Swagger UI Express 创建的客户端。它还没有工作,因为我们还没有实现任何路线。但是,您可以尝试它们并看着它们失败!

启动代码运行。让我们添加一些 Redis OM 到它,让它真正做点什么

# 设置客户端

首先,让我们设置一个客户端Client类是知道如何代表 Redis OM 与 Redis 对话的东西。一种选择是将我们的客户端放在自己的文件中并导出。这确保了应用程序只有一个实例,Client因此只有一个与 Redis Stack 的连接。由于 Redis 和 JavaScript 都(或多或少)是单线程的,所以它工作得很好。

让我们创建我们的第一个文件。在om文件夹中添加一个名为的文件client.js并添加以下代码:

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

还记得我们之前提到的*顶级 await东西吗?*就在那里!

请注意,我们从环境变量中获取 Redis URL。它由 Dotenv 放在那里并从我们的.env文件中读取。如果我们没有该.env文件或REDIS_URL我们的文件中没有属性.env,则此代码很乐意从实际环境变量中读取此值。

另请注意,该.open()方法方便地返回this. 这this(我可以再说一遍吗?我刚刚做到了!)让我们将客户端的实例化与客户端的打开链接起来。如果这不符合你的喜好,你总是可以这样写:

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

# 实体、模式和存储库

现在我们有一个连接到 Redis 的客户端,我们需要开始映射一些人。为此,我们需要定义 anEntity和 a Schema。让我们首先在文件夹中创建一个名为person.js的文件,并从Redis OM中om导入和类:client``client.js``Entity``Schema

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

# 实体

接下来,我们需要定义一个实体。AnEntity是当你使用它时保存数据的类——被映射到的东西。它是您创建、阅读、更新和删除的内容。任何扩展Entity的类都是实体。我们将Person用一行定义我们的实体:

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

# 架构

架构定义实体上的字段、它们的类型以及它们如何在内部映射到 Redis。默认情况下,实体映射到 JSON 文档。让我们创建我们的Schemain 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' }
})

当您创建 aSchema时,它会修改Entity您交给它的类(Person在我们的例子中),为您定义的属性添加 getter 和 setter。这些 getter 和 setter 接受和返回的类型是使用 type 参数定义的,如上所示。有效值为:stringnumberbooleanstring[]datepointtext

前三个完全按照你的想法做——它们定义了一个属性,即 a String、 a Number 或 a Booleanstring ]做你想做的事,特别是定义一个[Array 字符串。

date有点不同,但或多或少还是你所期望的。它定义了一个返回 a 的属性, Date 并且不仅可以使用 a 设置,还可以使用Date包含StringISO 8601 日期或Number带有 UNIX 纪元时间 (以毫秒为单位)的 a 进行设置。

Apoint将地球上某处的点定义为经度和纬度。它创建一个属性,该属性返回并接受具有 和 属性的简单longitude对象latitude。像这样:

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

字段text很像. string如果您只是读取和写入对象,它们是相同的。但是如果你想搜索它们,它们是非常非常不同的。稍后我们将更多地讨论搜索,但 tl;dr 是string字段只能在其整个值上匹配 - 不能部分匹配 - 并且最适合键,而text字段上启用了全文搜索并针对人类进行了优化- 可读的文本。

# 存储库

现在我们拥有了创建存储库所需的所有部分。ARepository是 Redis OM 的主接口。它为我们提供了读取、写入和删除特定Entity. 创建一个Repositoryinperson.js并确保它在我们开始实现 out API 时按照您的需要导出:

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

我们几乎完成了我们的存储库的设置。但是我们仍然需要创建一个索引,否则我们将无法搜索。我们通过调用来做到这一点.createIndex()。如果一个索引已经存在并且它是相同的,这个函数不会做任何事情。如果它不同,它将删除它并创建一个新的。添加调用.createIndex()person.js

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

这就是我们所需要的,person.js也是我们开始使用 Redis OM 与 Redis 对话所需的一切。这是完整的代码:

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()

现在,让我们在 Express 中添加一些路由。

# 设置人员路由器

让我们创建一个真正的 RESTful API,并将 CRUD 操作分别映射到 PUT、GET、POST 和 DELETE。我们将使用 Express Routers 来做到这一点,因为这会使我们的代码变得整洁。在文件夹中创建一个名为person-router.jsrouters文件,并在其中从RouterExpress 和. 然后创建并导出一个:personRepository``person.js``Router

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

export const router = Router()

导入和导出完成,让我们将路由器绑定到我们的 Express 应用程序。打开server.js并导入Router我们刚刚创建的:

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

然后添加personRouter到 Express 应用程序:

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

server.js现在应该是这样的:

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)

现在我们可以添加我们的路线来创建、读取、更新和删除人员。回到person-router.js文件,这样我们就可以做到这一点。

# 创建一个人

我们将首先创建一个人员,因为您需要在 Redis 中拥有人员,然后才能对其进行任何读取、写入或删除操作。在下面添加 PUT 路由。此路由将调用从请求正文.createAndSave()创建一个Person并立即将其保存到 Redis:

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

请注意,我们还返回了新创建的Person. 让我们通过使用 Swagger UI 实际调用我们的 API 来看看它是什么样子的。在浏览器中访问 http://localhost:8080 并尝试一下。Swagger 中的默认请求正文可用于测试。您应该会看到如下所示的响应:

{
  "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"
}

这正是我们交给它的一个例外:entityId. Redis OM 中的每个实体都有一个实体 ID,正如您可能已经猜到的那样,它是该实体的唯一 ID。它是我们调用时随机生成的.createAndSave()。你的会有所不同,所以请记下它。

您可以使用 RedisInsight 在 Redis 中查看这个新创建的 JSON 文档。继续并启动 RedisInsight,您应该会看到一个名称类似于Person:01FY9MWDTWW4XQNTPJ9XY9FPMN. 密钥的Person位来自我们实体的类名,字母和数字的序列是我们生成的实体 ID。单击它查看您创建的 JSON 文档。

您还将看到一个名为Person:index:hash. 这是 Redis OM 用来查看是否需要在.createIndex()调用时重新创建索引的唯一值。您可以放心地忽略它。

# 读一个人

创建下来,让我们添加一个 GET 路由来读取这个新创建的Person

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

这段代码从路由中使用的 URL 中提取一个参数——entityId我们之前收到的那个。它使用 上的.fetch()方法来使用 thatpersonRepository检索 a 。然后,它返回那个.Person``entityId``Person

让我们继续在 Swagger 中进行测试。您应该得到完全相同的响应。事实上,由于这是一个简单的 GET,我们应该能够将 URL 加载到我们的浏览器中。也可以通过导航到 http://localhost:8080/person/01FY9MWDTWW4XQNTPJ9XY9FPMN 进行测试,将实体 ID 替换为您自己的 ID。

现在我们可以读写了,让我们实现 HTTP 动词的REST。休息……明白了吗?

# 更新人员

让我们添加代码以使用 POST 路由更新人员:

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)
})

就像我们之前的路线一样,这段代码使用 the 来Person获取。但是,现在我们根据请求正文中的属性更改所有属性。如果其中任何一个缺失,我们将它们设置为. 然后,我们调用并返回更改后的.personRepository``entityId``null``.save()``Person

让我们在 Swagger 中也测试一下,为什么不呢?做一些改变。尝试删除一些字段。改完之后再读会得到什么?

# 删除人员

删除——我的最爱!记住孩子们,删除是 100% 压缩。删除的路线与读取的路线一样简单,但更具破坏性:

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

我想我们也应该测试一下这个。加载 Swagger 并练习路线。您应该使用刚刚删除的实体 ID 返回 JSON:

{
  "entityId": "01FY9MWDTWW4XQNTPJ9XY9FPMN"
}

就这样,它消失了!

# 所有的 CRUD

快速检查一下您到目前为止所写的内容。以下是您person-router.js文件的全部内容:

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 })
})

# 准备搜索

CRUD 完成,让我们做一些搜索。为了搜索,我们需要数据来搜索。还记得那个persons包含所有 JSON 文档和load-data.shshell 脚本的文件夹吗?它的时间到了。进入该文件夹并运行脚本:

cd persons
./load-data.sh

您应该得到一个相当详细的响应,其中包含来自 API 的 JSON 响应和您加载的文件的名称。像这样:

{"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

有点乱,但如果你没有看到这个,那就没用了!

现在我们有了一些数据,让我们添加另一个路由器来保存我们想要添加的搜索路由。在 routers 文件夹中创建一个名为search-router.js的文件,并使用导入和导出设置它,就像我们在中所做的那样person-router.js

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

export const router = Router()

Router以与server.js我们相同的方式导入personRouter:

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

然后添加searchRouter到 Express 应用程序:

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

路由器绑定,我们现在可以添加一些路由。

# 搜索所有东西

我们将在新的Router. 但第一个将是最简单的,因为它只会返回所有内容。继续并将以下代码添加到search-router.js

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

在这里,我们看到如何开始和完成搜索。搜索开始就像 CRUD 操作开始一样——在Repository. 但是我们不调用.createAndSave(), .fetch(), .save(), or .remove(),而是调用.search(). 与所有其他方法不同,.search()并不止于此。相反,它允许您构建一个查询(您将在下一个示例中看到),然后通过调用.return.all().

有了这条新路线,进入 Swagger UI 并练习/persons/all路线。您应该将使用 shell 脚本添加的所有人员视为 JSON 数组。

在上面的示例中,没有指定查询——我们没有构建任何东西。如果你这样做,你就会得到一切。这就是你有时想要的。但不是大多数时候。如果您只返回所有内容,这并不是真正的搜索。因此,让我们添加一条路线,让我们按姓氏查找人员。添加以下代码:

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)
})

在这个路由中,我们指定了一个我们想要过滤的字段和一个它需要相等的值。调用中的字段名称.where()是我们模式中指定的字段名称。该字段被定义为string,这很重要,因为该字段的类型决定了可用的查询方法。

在 a 的情况下string,有 just .equals(),它将查询整个字符串的值。为方便起见,别名为.eq().equal().equalTo()。你甚至可以通过调用来添加更多的语法糖,.is.does实际上并没有做任何事情,只是让你的代码更漂亮。像这样:

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

您还可以通过调用来反转查询.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()

在所有这些情况下,调用.return.all()执行我们在它和调用之间构建的查询.search()。我们也可以搜索其他字段类型。让我们添加一些路由来搜索 anumber和 aboolean字段:

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)
})

number字段按年龄过滤年龄大于或等于 21 的人。同样,有别名和语法糖:

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

但也有更多的查询方式:

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()

boolean字段正在按验证状态搜索人员。它已经包含了我们的一些语法糖。请注意,此查询将匹配缺失值或错误值。这就是我指定.not.true(). 您还可以调用.false()布尔字段以及.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()

所以,我们创建了一些路线,我没有告诉你测试它们。也许你无论如何都有。如果是这样,对你有好处,你叛逆。对于你们其他人,为什么不立即使用 Swagger 进行测试呢?而且,继续前进,只需在需要时对其进行测试。哎呀,使用提供的语法创建一些您自己的路线并尝试一下。不要让我告诉你如何过你的生活。

当然,仅查询一个字段是远远不够的。没问题,Redis OM 可以处理.and().or()喜欢这条路线:

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)
})

在这里,我只是展示了语法,.and()但当然,您也可以使用.or().

# 全文搜索

如果您在架构中定义了类型为 的字段text,则可以对其执行全文搜索。搜索字段的方式与text搜索 a 的方式不同string。Astring只能与.equals()并且必须匹配整个字符串。使用text字段,您可以在字符串中查找单词。

字段针对人类可读的文本进行了text优化,例如文章或歌词。这很聪明。它理解某些词(如aanthe)是常见的并忽略它们。它了解单词在语法上的相似之处,因此如果您搜索give ,**它也会匹配give 、givengivegive。它忽略了标点符号。

personalStatement让我们添加一个对我们的字段进行全文搜索的路由:

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)
})

注意.matches()函数的使用。这是唯一适用于text字段的。它需要一个字符串,该字符串可以是您要查询的一个或多个单词(以空格分隔)。让我们试试看。在 Swagger 中,使用这条路线搜索单词“walk”。您应该得到以下结果:

[
  {
    "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."
  }
]

请注意单词“walk”如何与包含“walks”的 Rupert Holmes 的个人陈述相匹配,以及如何与包含“walk”的 Chris Stapleton 相匹配。现在搜索“走路下雨”。您会看到,即使在他的个人陈述中找不到这两个词的确切文本,这也会返回 Rupert 的条目。但它们在语法上是相关的,所以它匹配它们。这称为词干提取,它是 Redis OM 所利用的 RediSearch 的一个非常酷的功能。

如果您搜索“a rain walk”,即使文本中没有单词“a”,您*仍然会匹配 Rupert 的条目。*为什么?因为它是一个常用词,对搜索没有多大帮助。这些常用词称为停用词,这是 Redis OM 免费获得的 RediSearch 的另一个很酷的功能。

# 寻找地球

RediSearch 和 Redis OM 都支持按地理位置搜索。您指定地球上的一个点、一个半径和该半径的单位,它会高兴地返回其中的所有实体。让我们添加一条路线来做到这一点:

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)
})

这段代码看起来与其他代码有些不同,因为我们定义要搜索的圆的方式是通过传递给.inRadius方法的函数完成的:

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

这个函数所做的只是接受一个 Circle 已经用默认值初始化的实例。我们通过调用各种构建器方法来覆盖这些值,以定义搜索的原点(即经度和纬度)、半径和测量半径的单位。有效单位是milesmetersfeetkilometers

让我们尝试一下路线。我知道我们可以在宾夕法尼亚州东部的经度 -75.0 和纬度 40.0 附近找到琼·杰特。所以使用半径为 20 英里的坐标。您应该收到以下回复:

[
  {
    "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."
  }
]

尝试扩大半径,看看还能找到谁。

# 添加位置跟踪

我们正在接近教程的结尾,但在我们开始之前,我想添加我在开头提到的位置跟踪部分。如果您已经做到了这一点,那么接下来的代码应该很容易理解,因为它并没有真正做任何我还没有谈到的事情。

location-router.js在文件夹中添加一个名为的新文件routers

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 } })
})

这里我们调用.fetch()来获取一个人,我们正在更新那个人的一些值——.location带有我们的经度和纬度的.locationUpdated属性以及带有当前日期和时间的属性。容易的东西。

要使用它Router,请将其导入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'

并将路由器绑定到路径:

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

就是这样。但这还不足以满足。它没有向您显示任何新内容,除了可能使用date字段。而且,它不是真正的位置跟踪。它只是显示这些人最后在哪里,没有历史。所以让我们添加一些!

要添加一些历史记录,我们将使用 Redis Stream。Streams 是一个很大的话题,但如果您不熟悉它们,请不要担心,您可以将它们视为一种存储在 Redis 键中的日志文件,其中每个条目代表一个事件。在我们的例子中,事件将是人四处走动或办理登机手续或其他任何事情。

但是有一个问题。Redis OM 不支持 Streams,尽管 Redis Stack 支持。那么我们如何在我们的应用程序中利用它们呢?通过使用 节点 Redis。Node Redis 是 Node.js 的低级 Redis 客户端,可让您访问所有 Redis 命令和数据类型。在内部,Redis OM 正在创建和使用 Node Redis 连接。您也可以使用该连接。或者更确切地说,可以告诉Redis OM使用您正在使用的连接。让我告诉你怎么做。

# 使用节点 Redis

client.jsom文件夹中打开。还记得我们是如何创建 Redis OMClient并调用.open()它的吗?

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

好吧,Client该类还有一个.use()采用 Node Redis 连接的方法。修改client.js以使用 Node Redis 打开与 Redis 的连接,然后.use()

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

就是这样。Redis OM 现在正在使用connection您创建的。请注意,我们同时导出client 。如果我们想在我们最新的路线中使用它,connection必须导出它。connection

# 使用 Streams 存储位置历史记录

要将事件添加到 Stream,我们需要使用 XADD 命令。Node Redis 将其公开为.xAdd(). 所以,我们需要.xAdd()在我们的路由中添加一个调用。修改location-router.js以导入我们的connection

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

然后在路由本身添加一个调用.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()`接受一个键名、一个事件 ID 和一个 JavaScript 对象,其中包含构成事件的键和值,即事件数据。对于键名,我们使用继承自(将返回类似)的`.keyName`属性与硬编码值相结合来构建字符串。我们传入我们的事件 ID,它告诉 Redis 根据当前时间和之前的事件 ID 生成它。我们正在传递具有经度和纬度属性的位置作为我们的事件数据。`Person``Entity``Person:01FYC7CTPKYNXQ98JSTBC37AS1``*

现在,每当执行此路线时,都会记录经度和纬度,并且事件 ID 将对时间进行编码。继续使用 Swagger 移动 Joan Jett 几次。

现在,进入 RedisInsight 并查看 Stream。您会在键列表中看到它,但如果单击它,您会收到一条消息,提示“此数据类型即将推出!”。如果您没有收到此消息,那么恭喜您,您活在未来!对于过去的我们来说,我们将只发出原始命令:

XRANGE Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory - +

这告诉 Redis 从存储在给定键名中的 Stream 中获取一系列值 -Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory在我们的示例中。接下来的值是开始事件 ID 和结束事件 ID。-是 Stream 的开始。+是结束。所以这会返回 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"

就这样,我们正在跟踪 Joan Jett。

# 包起来

所以,现在你知道如何使用 Express + Redis OM 来构建一个由 Redis Stack 支持的 API。而且,在这个过程中,你已经得到了一些相当不错的开始代码。好买卖!如果您想了解更多信息,可以查看 Redis OM 的 文档。它涵盖了 Redis OM 的全部功能。

感谢您花时间解决这个问题。我真诚地希望你觉得它有用。如果您有任何问题, Redis Discord 服务器 是迄今为止获得答案的最佳场所。加入服务器并询问!

Last Updated: 4/18/2023, 8:45:33 AM