学习

如何使用 Redis 构建 HackerNews 克隆

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

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

这是一个基于 React、NextJS 作为前端,NodeJS、ExpressJS 和 Redis 作为后端的 HackerNews 克隆。该应用程序使用 JSON 存储数据,并在 Redis Stack 中搜索。

步骤 1. 安装先决条件#

安装以下软件包

  • NPM v7.8.0
  • NODE v15.10.0

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

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

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

点击此链接 创建包含 2 个数据库的 Redis Cloud 帐户,使用 Redis Stack。

保存数据库端点 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 的热门故事。首先创建一个版主,版主: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
  • 获取并在用户集合中增加下一个 ID。
 GET user:id-indicator // 63
 INCR user:id-indicator  // 64 will be next user id, 63 is current user id
  • 创建用户: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

项目列表页面#

  • 检查用户是否已在特定项目上切换隐藏属性。
 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}]]
  • 获取过去一周内发布的项目
 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 是比当前时间戳早一周的时间戳

 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 .
  • 查找项目: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 中。
  • 创建索引

创建模式时,应创建索引。

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

如果模式已更改,则应删除/更新索引

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

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

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

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

 HSET user:andy username "andy" email "andy@gmail.com" 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"

参考#