Redis OM Node.js
了解如何使用 Redis Stack 和 Node.js 进行构建
本教程将向您展示如何使用 Node.js 和 Redis Stack 构建 API。
我们将使用 Express 和 Redis OM 来执行此操作,并且我们假设您对 Express 有基本了解。
我们将构建的 API 是一个简单且相对 RESTful 的 API,用于读取、写入和查找有关人员的数据:名字、姓氏、年龄等。我们还将添加一个简单的定位跟踪功能,仅为了增加一些额外的趣味性。
但在我们开始编写代码之前,让我们先描述一下 Redis OM 是什么。
先决条件
与任何与软件相关的事物一样,您需要在开始之前安装一些依赖项
- Node.js 14.8+:在本教程中,我们使用 JavaScript 的顶级
await
功能,该功能在 Node 14.8 中引入。因此,请确保您使用该版本或更高版本。 - Redis 堆栈: 你需要一个 Redis 堆栈版本,可以在你的机器上本地运行或在云端运行。
- 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
将所有 JSON 文件加载到 API 中。
有两个空文件夹,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 堆栈),现在是时候进行更改了。完成后,你应该能够运行该应用
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
文件或在 .env
文件中没有 REDIS_URL
属性,则此代码将很乐意从实际环境变量中读取此值。
另请注意,.open()
方法方便地返回 this
。此 this
(我可以再次说this 吗?我刚说了!)让我们将客户端的实例化与客户端的打开链接在一起。如果您不喜欢这样,您始终可以像这样编写
/* create and open the Redis OM Client */
const client = new Client()
await client.open(url)
实体、架构和存储库
现在我们有了连接到 Redis 的客户端,我们需要开始映射一些人员。为此,我们需要定义一个 Entity
和一个 Schema
。让我们首先在 om
文件夹中创建一个名为 person.js
的文件,并从 client.js
导入 client
,以及从 Redis OM 导入 Entity
和 Schema
类
import { Entity, Schema } from 'redis-om'
import client from './client.js'
实体
接下来,我们需要定义一个实体。Entity
是在您使用数据时保存数据的类——正在映射到的内容。它是您创建、读取、更新和删除的内容。任何扩展 Entity
的类都是一个实体。我们将使用一行定义我们的 Person
实体
/* our entity */
class Person extends Entity {}
架构
架构定义实体中的字段、它们的类型以及它们如何内部映射到 Redis。默认情况下,实体映射到 JSON 文档。让我们在 person.js
中创建我们的 Schema
/* 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' }
})
创建 Schema
时,它将修改你递交的 Entity
类(在本例中为 Person
),为定义的属性添加 getter 和 setter。getter 和 setter 接受和返回的类型使用如上所示的类型参数定义。有效值包括:string
、number
、boolean
、string[]
、date
、point
和 text
。
前三个完全符合你的预期——它们定义了一个属性,该属性是 String
、Number
或 Boolean
。string[]
也符合你的预期,具体来说,它定义了一个字符串的 Array
。
date
有点不同,但仍然或多或少符合你的预期。它定义了一个返回 Date
的属性,不仅可以使用 Date
设置,还可以使用包含 ISO 8601 日期或带有 UNIX 纪元时间(以毫秒为单位)的 Number
设置。
point
定义了地球上某个地方作为经度和纬度。它创建一个属性,该属性返回并接受一个简单的对象,该对象的属性为 longitude
和 latitude
。如下所示
let point = { longitude: 12.34, latitude: 56.78 }
text
字段非常类似于 string
。如果你只是读写对象,它们是相同的。但是,如果你想搜索它们,它们非常非常不同。我们稍后会详细讨论搜索,但简而言之,string
字段只能匹配其整个值——没有部分匹配——最适合用作键,而 text
字段启用了全文搜索,并且针对人类可读文本进行了优化。
存储库
现在我们拥有创建存储库所需的所有部分。Repository
是 Redis OM 的主接口。它为我们提供了读取、写入和移除特定Entity
的方法。在 person.js
中创建一个 Repository
,并确保将其导出,因为在开始实现我们的 API 时需要它
/* use the client to create a Repository just for Persons */
export const personRepository = new Repository(personSchema, client)
我们几乎完成了存储库的设置。但我们仍然需要创建一个索引,否则将无法搜索。我们通过调用 .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 中添加一些路由。
设置 Person 路由器
让我们创建一个真正的 RESTful API,其中 CRUD 操作分别映射到 PUT、GET、POST 和 DELETE。我们将使用Express 路由器来执行此操作,因为这会使我们的代码简洁明了。在 routers
文件夹中创建一个名为 person-router.js
的文件,并在其中从 Express 导入 Router
,从 person.js
导入 personRepository
。然后创建并导出一个 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()
时,它被随机生成。您的将有所不同,因此请注意它。
您可以在 Redis 中使用 RedisInsight 查看此新创建的 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
。它在 personRepository
上使用 .fetch()
方法来使用该 entityId
检索 Person
。然后,它返回该 Person
。
让我们继续在 Swagger 中对其进行测试。您应该得到完全相同的响应。事实上,由于这是一个简单的 GET,我们应该能够直接将 URL 加载到我们的浏览器中。通过导航到 http://localhost:8080/person/01FY9MWDTWW4XQNTPJ9XY9FPMN(用您自己的实体 ID 替换实体 ID)来对其进行测试。
现在我们已经可以读写了,让我们实现 HTTP 动词的REST。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)
})
此代码使用 entityId
从 personRepository
中获取 Person
,就像我们之前的路由一样。但是,现在我们根据请求正文中的属性更改所有属性。如果其中任何一个丢失,我们将其设置为 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 已完成,让我们进行一些搜索。为了搜索,我们需要搜索数据。还记得带有所有 JSON 文档和 load-data.sh
shell 脚本的 persons
文件夹吗?现在是时候了。进入该文件夹并运行脚本
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
有点乱,但如果您看不到这一点,那么它就没有用!
现在我们有了一些数据,让我们添加另一个路由器来保存我们想要添加的搜索路由。在路由器文件夹中创建一个名为 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()
或 .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
,这一点很重要,因为字段的类型决定了可用于查询它的方法。
对于 string
,只有 .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()
的调用之间构建的查询。我们还可以搜索其他字段类型。让我们添加一些路由来搜索 number
和 boolean
字段
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()
的原因。您还可以在布尔字段以及 .equals
的所有变体上调用 .false()
。
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
字段的方式与搜索 string
的方式不同。string
只能与 .equals()
比较,并且必须匹配整个字符串。使用 text
字段,您可以在字符串中查找单词。
text
字段针对人类可读文本进行了优化,例如文章或歌词。它非常聪明。它理解某些单词(如 a、an 或 the)很常见并会忽略它们。它理解单词在语法上的相似性,因此如果您搜索 give,它也会匹配 gives、given、giving 和 gave。并且它会忽略标点符号。
让我们添加一个针对我们的 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."
}
]
请注意,对于包含“walks”的 Rupert Holmes 的个人陈述,单词“walk”匹配,对于包含“walk”的 Chris Stapleton 的个人陈述,单词“walk”也匹配。现在搜索“walk raining”。您会看到,即使在他的个人陈述中没有找到这两个单词的确切文本,但它只返回了 Rupert 的条目。但它们在语法上是相关的,所以它匹配了它们。这称为词干提取,这是 Redis OM 利用的 Redis Stack 的一项非常酷的功能。
如果你搜索“a rain walk”,即使文本中没有“a”这个词,你仍然会匹配到 Rupert 的条目。为什么?因为它是一个常见词,在搜索中没有太大帮助。这些常见词被称为停用词,这是 Redis Stack 的另一个很酷的功能,Redis OM 可以免费获得。
搜索地球
Redis Stack 和 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 附近找到 Joan Jett,该位置在宾夕法尼亚州东部。因此,使用这些坐标,半径为 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."
}
]
尝试扩大半径,看看你还能找到谁。
添加位置跟踪
我们正在接近本教程的结尾,但在结束之前,我想添加我在开头提到的位置跟踪部分。如果你已经理解到这一步,那么接下来这段代码应该很容易理解,因为它并没有做任何我之前没有讲过的事情。
在 routers
文件夹中添加一个名为 location-router.js
的新文件
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 流。流是一个大话题,但如果你不熟悉它们,请不要担心,你可以将它们想象成存储在 Redis 密钥中的日志文件,其中每个条目都表示一个事件。在我们的例子中,该事件可能是人员移动或签到或其他事件。
但有一个问题。尽管 Redis Stack 支持流,但 Redis OM 并不支持。那么,我们如何在应用程序中利用它们呢?通过使用 Node Redis。Node Redis 是 Node.js 的低级 Redis 客户端,它让你可以访问所有 Redis 命令和数据类型。在内部,Redis OM 正在创建和使用 Node Redis 连接。你也可以使用该连接。或者更确切地说,可以“告知”Redis OM 使用你正在使用的连接。让我向你展示如何操作。
使用 Node Redis
在 `om` 文件夹中打开 `client.js`。还记得我们如何创建 Redis OM `Client` 然后调用其上的 `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`,则必须导出它。
使用流存储位置历史记录
要向流添加事件,我们需要使用 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 对象,即事件数据。对于键名,我们使用 Person
从 Entity
继承的 .keyName
属性(将返回类似 Person:01FYC7CTPKYNXQ98JSTBC37AS1
的内容)与硬编码值结合来构建一个字符串。我们为事件 ID 传入 *
,这会告诉 Redis 根据当前时间和之前的事件 ID 自动生成它。我们还将位置(包含经度和纬度属性)作为我们的事件数据传入。
现在,每当执行此路由时,经度和纬度都会被记录,并且事件 ID 将对时间进行编码。继续使用 Swagger 让 Joan Jett 四处走动几次。
现在,进入 RedisInsight 并查看流。你会在键列表中看到它,但如果你点击它,你会收到一条消息,提示“此数据类型即将推出!”。如果你没有收到此消息,恭喜你,你生活在未来!对于我们这些生活在过去的人来说,我们只需发出原始命令即可
XRANGE Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory - +
这会告诉 Redis 从存储在给定键名中的流中获取一系列值——在我们的示例中为 Person:01FYC7CTPKYNXQ98JSTBC37AS1:locationHistory
。接下来的值是起始事件 ID 和结束事件 ID。-
是流的开头。+
是结尾。因此,这会返回流中的所有内容
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 服务器 是迄今为止获得解答的最佳场所。加入服务器并提问吧!