学习

如何使用 Redis 构建 HackerNews 克隆

Ajeet Raina
作者
Ajeet Raina, Redis 前开发者增长经理

Hacker News(有时简称 HN)是一个专注于计算机科学和创业的社交新闻网站。它是 Graham 的公司 Y Combinator 的一个项目,作为 Graham 共同开发的 Arc 编程语言的实际应用而发展起来。

这是一个 HackerNews 克隆,前端使用 React、NextJS,后端使用 NodeJS、ExpressJS 和 Redis。此应用程序使用 JSON 存储数据,并使用 Redis Stack 中的 Search 进行搜索。

步骤 1. 安装先决条件#

安装以下软件包

  • NPM v7.8.0
  • NODE v15.10.0

步骤 2. 创建 Redis Cloud 数据库#

Redis 是一个开源、内存中、键值数据存储,最常被用作主数据库、缓存、消息代理和队列。Redis 在开发者中很受欢迎,因为它提供亚毫秒级的响应时间,支持游戏、金融科技、广告技术、社交媒体、医疗保健和物联网等行业中快速强大的实时应用。

Redis Cloud 是一个完全托管的云服务,用于以高可用和可伸缩的方式托管和运行您的 Redis 数据集,具有可预测且稳定的顶级性能。Redis Cloud 允许您通过云运行 Redis 服务器,并通过 RedisInsight、Redis 命令行以及客户端工具等多种方式访问实例。您可以通过 Redis Cloud 的 Redis Heroku 附加组件快速轻松地启动和运行您的应用程序,只需告诉我们您需要多少内存,即可立即开始使用您的第一个 Redis 数据库。然后,您可以添加更多 Redis 数据库(每个都在专用进程中以非阻塞方式运行),并增加或减少您的计划内存大小,而不会影响您现有数据。

点击此链接 创建一个包含 2 个 Redis Stack 数据库的 Redis Cloud 账户。

保存数据库端点 URL 和密码以供将来参考

步骤 3. 克隆仓库#

 git clone https://github.com/redis-developer/redis-hacker-news-demo
 cd redis-hacker-news-demo

步骤 4. 设置环境变量#

将 .env.sample 复制到 .env 并按如下所示提供值

 MAILGUN_API_KEY=YOUR_VALUE_HERE
 SEARCH_REDIS_SERVER_URL=redis://redis-XXXXX.c10.us-east-1-2.ec2.cloud.redislabs.com:10292
 SEARCH_REDIS_PASSWORD=ABCDXYZbPXHWsC
 JSON_REDIS_SERVER_URL=redis://redis-XXXXX.c14.us-east-1-2.ec2.cloud.redislabs.com:14054
 JSON_REDIS_PASSWORD=ABCDXYZA3tzw2XYMPi2P8UPm19D
 LOG_LEVEL=1
 USE_REDIS=1
 REDIS_REINDEX=
 PRODUCTION_WEBSITE_URL=i

步骤 5. 运行开发环境#

 npm install
 npm run dev

步骤 6. 拉取 Hacker News API 以填充数据库#

使用 API,它可以拉取最新的 hackernews 数据。接下来,您需要填充来自 hacker news 的热门故事。首先创建一个协调员(moderator),用户名为 moderator,密码为 password123

 node ./backend/scripts/seed.js

步骤 7. 访问 HackerNews URL#

打开 https://localhost:3001,您应该能看到如下所示的 HackerNews 登录屏幕

工作原理#

按屏幕划分#

注册#

  • 确保用户(用户名为 andy1)不存在。
 FT.SEARCH idx:user @username:"andy1" NOCONTENT LIMIT 0 1 SORTBY _id DESC
  • 获取并增加 users 集合中的下一个 id。
 GET user:id-indicator // 63
 INCR user:id-indicator  // 64 will be next user id, 63 is current user id
  • 创建 user:63 哈希和 json。(json 也收集 authToken 和密码哈希等)
  HSET user:63 username andy1 email  created 1615569194 karma 0 about  showDead false isModerator false shadowBanned false banned false _id 63
  JSON.SET user:63 .
 '{"username":"andy1","password":"$2a$10$zy8tsCske8MfmDX5CcWMce5S1U7PJbPI7CfaqQ7Bo1PORDeqJxqhe","authToken":"AAV07FIwTiEkNrPj0x1yj6BPJQSGIPzV0sICw2u0","  authTokenExpiration":1647105194,"email":"","created":1615569194,"karma":0,"showDead":false,"isModerator":false,"shadowBanned":false,"banned":false,"_id":63}'

登录#

  • 查找用户
 FT.SEARCH idx:user  @username:"andy1" NOCONTENT LIMIT 0 1 SORTBY _id DESC
  • 确保密码正确
 JSON.MGET user:63 .
  • 比较密码和新密码哈希,如果成功则创建 cookie

项目列表页#

  • 检查用户是否在特定项目上切换了 hidden 属性。
 FT.SEARCH idx:user-hidden  @username:"andy1" NOCONTENT LIMIT 0 10000 SORTBY _id DESC
 // Result - [0, "item:4"]
  • 如果不是 null
 FT.SEARCH idx:item  (-(@id:"item:4")) (@dead:"false") NOCONTENT LIMIT 0 30 SORTBY _id ASC
  • 如果是空数组
 FT.SEARCH idx:item (@dead:"false") NOCONTENT LIMIT 0 30 SORTBY _id ASC
 // Result - [3,"item:1","item:2","item:3"]
  • 使用 JSON.MGET 从 Redis 获取所有项目
 JSON.MGET item:1 item:2 item:3 .
 // Result - [{"id":"bkWCjcyJu5WT","by":"todsacerdoti","title":"Total Cookie
 Protection","type":"news","url":"https://blog.mozilla.org/security/2021/02/23/total-cookie-
 protection/","domain":"mozilla.org","points":1,"score":1514,"commentCount":0,"created":1614089461,"dead":false,"_id":3}]]
  • 获取过去 1 周内发布的项目
 FT.SEARCH idx:item  (@created:[(1615652598 +inf]) (@dead:"false") NOCONTENT LIMIT 0 0 SORTBY _id DESC
 // Result - [13,"item:19","item:17","item:16","item:15","item:14","item:13","item:12","item:11","item:8","item:5","item:4","item:3","item:1"]
注意

在此示例中,1615652598 是比当前时间戳早 1 周的时间戳

 JSON.MGET item:19 item:17 item:16 item:15 item:14 item:13 item:12 item:11 item:8 item:5 item:4 item:3 item:1 .
 // Result - the JSON of selected items

项目详情#

  • 首先获取项目对象
 JSON.MGET item:1 .
  • 查找 item:1 的根评论
 FT.SEARCH idx:comment  (@parentItemId:"kDiN0RhTivmJ") (@isParent:"true") (@dead:"false") NOCONTENT LIMIT 0 30 SORTBY points ASC
 // Result - [3,"comment:1","comment:2","comment:12"]
  • 获取这些评论
 JSON.MGET comment:1 comment:2 comment:12 .
 // one comment example result - {"id":"jnGWS8TTOecC","by":"ploxiln","parentItemId":"kDiN0RhTivmJ","parentItemTitle":"The Framework
 Laptop","isParent":true,"parentCommentId":"","children":[13,17,20],"text":"I don't see any mention of the firmware and drivers efforts for this.
 Firmware and drivers always end up more difficult to deal with than expected.<p>The Fairphone company was surprised by difficulties upgrading and
 patching android without support from their BSP vendor, causing many months delays of updates _and_ years shorter support life than they were
 planning for their earlier models.<p>I purchased the Purism Librem 13 laptop from their kickstarter, and they had great plans for firmware and
 drivers, but also great difficulty following through. The trackpad chosen for the first models took much longer than expected to get upstream linux
 support, and it was never great (it turned out to be impossible to reliably detect their variant automatically). They finally hired someone with
 sufficient skill to do the coreboot port _months_ after initial units were delivered, and delivered polished coreboot firmware for their initial
 laptops _years_ after they started the kickstarter.<p>So, why should we have confidence in the firmware and drivers that Framework will deliver
 :)","points":1,"created":1614274058,"dead":false,"_id":12}
  • 使用每个评论的子评论,获取子评论
 FT.SEARCH idx:comment  (@dead:"false") (@_id:("3"|"7"|"11")) NOCONTENT LIMIT 0 10000 SORTBY _id DESC
  • 重复此过程直到所有评论都解析完毕

提交#

  • 获取下一个项目的 id 并增加它
 GET item:id-indicator
 // Result - 4
 SET item:id-indicator 5
  • 创建哈希和索引
 HSET item:4 id iBi8sU4HRcZ2 by andy1 title Firebase trends type ask url  domain  text Firebase Performance Monitoring is a service that helps you to
 gain insight into the performance characteristics of your iOS, Android, and web apps. points 1 score 0 created 1615571392 dead false _id 4
 JSON.SET item:4 . '{"id":"iBi8sU4HRcZ2","by":"andy1","title":"Firebase trends","type":"ask","url":"","domain":"","text":"Firebase Performance
 Monitoring is a service that helps you to gain insight into the performance characteristics of your iOS, Android, and web
 apps.","points":1,"score":0,"commentCount":0,"created":1615571392,"dead":false,"_id":4}'

更新资料#

  • 获取用户
 FT.SEARCH idx:user  (@username:"andy1") NOCONTENT LIMIT 0 1 SORTBY _id DESC
 JSON.MGET user:63 .
  • 更新新用户
 HSET user:63 username andy1 email  created 1615569194 karma 1 about I am a software engineer. showDead false isModerator false shadowBanned false
 banned false _id 63
 JSON.SET user:63 .
'{"username":"andy1","password":"$2a$10$zy8tsCske8MfmDX5CcWMce5S1U7PJbPI7CfaqQ7Bo1PORDeqJxqhe","authToken":"KJwPLN1idyQrMp5qEY5hR3VhoPFTKRcC8Npxxoju","   authTokenExpiration":1647106257,"email":"","created":1615569194,"karma":1,"about":"I am a software
 engineer.","showDead":false,"isModerator":false,"shadowBanned":false,"banned":false,"_id":63}'

协调员日志屏幕#

  • 查找所有协调员日志
 FT.SEARCH idx:moderation-log * NOCONTENT LIMIT 0 0 SORTBY _id DESC
  // Result - [1,"moderation-log:1"]
  • 获取该协调员日志
 JSON.MGET moderation-log:1 .
  • 获取包含 "fa" 的项目
 FT.SEARCH idx:item  (@title:fa*) (-(@id:"aaaaaaaaa")) (@dead:"false") NOCONTENT LIMIT 0 30 SORTBY score ASC
  // Result - [2,"item:18","item:16"]
  • 通过 json 获取这些项目
 JSON.MGET item:18 item:16 .

示例命令#

字段有两种类型:索引字段和非索引字段。#

  1. 1.索引字段将使用 HSET/HGET 存储在哈希中。
  2. 2.非索引字段将存储在 JSON 中。
  • 创建一个索引

创建 schema 后,应该创建索引。

 FT.CREATE idx:user ON hash PREFIX 1 "user:" SCHEMA username TEXT SORTABLE email TEXT SORTABLE karma NUMERIC SORTABLE
  • 删除搜索索引

如果 schema 发生变化,应该删除/更新索引

 FT.DROPINDEX idx:user
  • 获取搜索信息

验证字段是否正确索引。如果没有,将更新索引字段或删除/重新创建索引。

 FT.INFO idx:user
  • 创建一个新用户

这将需要新的哈希和新的 JSON 记录

 HSET user:andy username "andy" email "[email protected]" karma 0
 JSON.SET user:andy '{"passoword": "hashed_password", "settings": "{ \"showDead\": true }" }'
  • 更新用户
 HSET user:1 username "newusername"
 JSON.SET user:andy username "newusername"
  • 查找用户名为 'andy' 的用户
  1. 1.首先查找用户的哈希
 FT.SEARCH idx:user '@username:{andy}'

2. 获取 JSON 对象以获取相关的 JSON 对象

 JSON.GET user:andy
  • 查找 id 为 andy1 或 andy2 的用户
 FT.SEARCH idx:user '@id:("andy1"|"andy2")'
  • 查找 id 不是 andy1 或 andy2 的用户
 FT.SEARCH idx:user '(-(@id:("andy1"|"andy2")))'
  • 查找 id 为 andy1 或用户名为 andy 的用户
 FT.SEARCH idx:user '(@id:"andy1") | (@username:"andy")'
  • 查找 id 为 andy1 且用户名为 andy 的用户
 FT.SEARCH idx:user '(@id:"andy1") (@username:"andy")'
  • 查找按用户名排序的前 10 个用户
 FT.SEARCH idx:user '*' LIMIT 0 10 SORTBY username ASC
  • 查找接下来的 10 个用户
 FT.SEARCH idx:user '*' LIMIT 10 20 SORTBY username ASC
  • 从多个键的 JSON 中获取
 JSON.MGET idx:user "andy1" "andy2" "andy3"

参考资料#