# Node.js 的 Redis OM
了解如何使用 Redis Stack 和 Node.js 进行构建
本教程将向您展示如何使用 Node.js 和 Redis Stack 构建 API。
我们将使用 Express
和 Redis 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
、 .NET
和 Spring
的风格和教程。
本教程将帮助您开始使用 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
.
有两个空文件夹om
和routers
. 该om
文件夹是所有 Redis OM 代码所在的位置。该routers
文件夹将保存我们所有 Express 路线的代码。
# 配置并运行
如果有点薄,启动代码完全可以运行。在我们继续编写实际代码之前,让我们配置并运行它以确保它可以正常工作。首先,获取所有依赖项:
npm install
然后,在根目录中设置一个.env
Dotenv 可以使用的文件。根目录中有一个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 文档。让我们创建我们的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' }
})
当您创建 aSchema
时,它会修改Entity
您交给它的类(Person
在我们的例子中),为您定义的属性添加 getter 和 setter。这些 getter 和 setter 接受和返回的类型是使用 type 参数定义的,如上所示。有效值为:string
、number
、boolean
、string[]
、date
、point
和text
。
前三个完全按照你的想法做——它们定义了一个属性,即 a String
、 a Number
或 a Boolean
。string ]
做你想做的事,特别是定义一个[Array
字符串。
date
有点不同,但或多或少还是你所期望的。它定义了一个返回 a 的属性, Date
并且不仅可以使用 a 设置,还可以使用Date
包含String
ISO 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
. 创建一个Repository
inperson.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.js
的routers
文件,并在其中从Router
Express 和. 然后创建并导出一个: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.sh
shell 脚本的文件夹吗?它的时间到了。进入该文件夹并运行脚本:
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
优化,例如文章或歌词。这很聪明。它理解某些词(如a、an或the)是常见的并忽略它们。它了解单词在语法上的相似之处,因此如果您搜索give ,**它也会匹配give 、given、give和give。它忽略了标点符号。
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
已经用默认值初始化的实例。我们通过调用各种构建器方法来覆盖这些值,以定义搜索的原点(即经度和纬度)、半径和测量半径的单位。有效单位是miles
、meters
、feet
和kilometers
。
让我们尝试一下路线。我知道我们可以在宾夕法尼亚州东部的经度 -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.js
在om
文件夹中打开。还记得我们是如何创建 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 服务器
是迄今为止获得答案的最佳场所。加入服务器并询问!