用于 Node.js 的 RedisOM

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

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

我们将使用ExpressRedis OM来实现这一点,并且我们假设您对 Express 有基本的了解。

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

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

先决条件

与任何软件相关的内容一样,您需要在开始之前安装一些依赖项:

  • Node.js 14.8+:在本教程中,我们使用 Node 14.8 中引入的 JavaScript 顶级await功能。因此,请确保您使用的是该版本或更高版本。
  • Redis Stack:您需要一个 Redis Stack 版本,可以在您的机器上本地运行,也可以在云中运行。
  • Redis Insight:我们将使用它来查看 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— — 将使用 将所有 JSON 文件加载到 API 中curl

有两个空文件夹,omroutersom所有 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 对话。一种选择是将我们的客户端放在其自己的文件中并将其导出。这可确保应用程序有且只有一个 Redis Stack 实例,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 的客户端,我们需要开始映射一些人。为此,我们需要定义一个EntitySchema。让我们首先person.js在文件夹中创建一个名为的文件,并从Redis OM 中om导入和类:clientclient.jsEntitySchema

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

实体

接下来,我们需要定义一个实体Entity实体是用于保存您使用的数据的类,即要映射到的对象。它是您创建、读取、更新和删除的对象。任何扩展的类Entity都是实体。我们将Person用一行代码定义我们的实体:

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

架构

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

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

前三个函数的作用正如您所想的那样 — — 它们定义了一个属性,即 a String、 aNumber或 a Booleanstring[]它们的作用也正如您所想的那样,具体来说是定义了一个Array字符串。

date略有不同,但大致符合您的预期。它定义了一个返回 的属性,Date并且不仅可以使用 来设置,Date还可以使用String包含ISO 8601日期的 或UNIX 纪元时间(以毫秒Number为单位)的来设置。

A将地球上某个点定义为经度和纬度。它创建一个属性,该属性返回并接受具有和point属性的简单对象。如下所示:longitudelatitude

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

字段text与 非常相似string。如果您只是读取和写入对象,它们是相同的。但是如果您想搜索它们,它们就非常不同了。我们稍后会进一步讨论搜索,但简而言之,string字段只能匹配其整个值(不能部分匹配),并且最适合键,而text字段启用了全文搜索并针对人类可读的文本进行了优化。

存储库

现在,我们已经拥有了创建存储库所需的所有部分。 ARepository是 Redis OM 的主要接口。它为我们提供了读取、写入和删除特定 的方法Entity。创建一个Repositoryinperson.js并确保它已导出,因为当我们开始实现 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 中添加一些路线。

设置人员路由器

让我们创建一个真正的 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()。您的 ID 会有所不同,因此请记下它。

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

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

读懂一个人

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

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

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

让我们继续在 Swagger 中测试一下。您应该会得到完全相同的响应。事实上,由于这是一个简单的 GET,我们应该能够将 URL 加载到浏览器中。通过导航到 http://localhost:8080/person/01FY9MWDTWW4XQNTPJ9XY9FPMN 进行测试,将实体 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)
})

此代码使用Person从获取,就像我们之前的路由一样。但是,现在我们根据请求正文中的属性更改所有属性。如果缺少任何属性,我们将它们设置为。然后,我们调用并返回更改后的。personRepositoryentityIdnull.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

有点乱,但如果你没看到这个,那么它就没有起作用!

现在我们有了一些数据,让我们添加另一个路由器来保存我们要添加的搜索路线。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()。我们也可以搜索其他字段类型。让我们添加一些搜索numberboolean字段的路由:

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搜索字段的方式与string搜索 a 的方式不同。astring只能与整个字符串进行比较,.equals()并且必须匹配整个字符串。使用 atext字段,您可以查找字符串中的单词。

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

让我们添加一条针对我们的字段进行全文搜索的路线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”与 Rupert Holmes 包含“walks”的个人陈述相匹配,也与 Chris Stapleton 包含“walk”的个人陈述相匹配。现在搜索“walk raining”。您会发现,这只返回 Rupert 的条目,尽管这两个词的确切文本都没有在他的个人陈述中找到。但它们在语法上相关,因此匹配了它们。这称为词干提取,它是 Redis Stack 的一个非常酷的功能,Redis OM 利用了它。

如果您搜索“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。我们通过调用各种构建器方法来覆盖这些值,以定义搜索的原点(即经度和纬度)、半径以及测量半径的单位。有效单位为milesmetersfeetkilometers

让我们尝试一下这条路线。我知道我们可以在经度 -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."
  }
]

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

添加位置追踪

教程即将结束,但在结束之前,我想补充一下我在开头提到的位置跟踪部分。如果你已经读到这里,那么接下来这段代码应该很容易理解,因为它并没有做我之前没有提到过的事情。

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 支持。那么我们如何在应用程序中利用它们呢?通过使用Node Redis。Node Redis 是 Node.js 的低级 Redis 客户端,可让您访问所有 Redis 命令和数据类型。在内部,Redis OM 正在创建和使用 Node Redis 连接。您也可以使用该连接。或者,可以指示 Redis OM使用您正在使用的连接。让我向您展示如何操作。

使用 Node 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.jsconnection

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 生成它。我们传入位置(具有经度和纬度属性)作为事件数据。PersonEntityPerson:01FYC7CTPKYNXQ98JSTBC37AS1*

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

现在,进入 Redis Insight 并查看 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"

就这样,我们开始追踪琼·杰特。

包起来

现在,您知道如何使用 Express + Redis OM 构建由 Redis Stack 支持的 API。而且,您已经在此过程中获得了一些相当不错的入门代码。太棒了!如果您想了解更多信息,可以查看Redis OM 的文档。它涵盖了 Redis OM 的全部功能。

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

给此页面评分
返回顶部 ↑