首页 > 文章列表 > 使用Node.js和Redis构建一个商店查找器

使用Node.js和Redis构建一个商店查找器

Redis nodejs 商店查找器
220 2023-09-03

访问任何连锁餐厅或商店的网站,您可能会找到一个“商店查找器”:一个看似简单的小页面,您可以在其中输入您的地址或邮政编码,它会提供您附近的位置。作为客户,这很棒,因为您可以找到附近的内容,并且业务影响是显而易见的。

构建“商店查找器”实际上是一项具有挑战性的任务。在本教程中,我们将介绍如何在 Node.js 和 Redis 中使用地理空间数据以及构建基本的商店查找器的基础知识。

我们将使用 Redis 的“geo”命令。这些命令是在 3.2 版本中添加的,因此您需要将其安装在您的开发计算机上。让我们做一个简短的检查 - 启动 redis-cli 并输入 GEOADD。您应该看到如下所示的错误消息:

(error) ERR wrong number of arguments for 'GEOADD' command

尽管出现错误消息,但这是一个好兆头 - 它表明您有命令 GEOADD。如果运行该命令并收到以下错误:

(error) ERR unknown command 'GEOADD'

在继续之前,您需要下载、构建并安装支持地理命令的 Redis 版本。

现在您已经有了受支持的 Redis 服务器,让我们来浏览一下 geo 命令。 Redis 有 6 个直接涉及地理空间索引的命令: GEOADDGEOHASHGEOPOSGEODISTGEORADIUSGEORADIUSBYMEMBER

让我们从 GEOADD 开始。正如您可能想象的那样,此命令添加一个地理空间项目。它有四个必需参数:键、经度、纬度和成员。键就像一个分组,代表键空间中的单个值。经度和纬度显然是浮点数的坐标;请注意这些值的顺序,因为它们可能与您习惯看到的顺序相反。最后,“成员”是您识别位置的方式。在 redis-cli 中,运行以下命令:

geoadd va-universities -76.493 37.063 christopher-newport-university
geoadd va-universities -76.706944 37.270833 college-of-william-and-mary
geoadd va-universities -78.868889 38.449444 james-madison-university
geoadd va-universities -78.395833 37.297778 longwood-university
geoadd va-universities -76.2625 36.8487 norfolk-state-university
geoadd va-universities -76.30522 36.88654 old-dominion-university
geoadd va-universities -80.569444 37.1275 radford-university
geoadd va-universities -77.475 38.301944 university-of-mary-washington
geoadd va-universities -78.478889 38.03 university-of-virginia
geoadd va-universities -82.576944 36.978056 uva-wise
geoadd va-universities -77.453255 37.546615 virginia-commonwealth-university
geoadd va-universities -79.44 37.79 virginia-military-institute
geoadd va-universities -77.425556 37.242778 virginia-state-university
geoadd va-universities -80.425 37.225 virginia-tech

这是添加多个条目的常用方法,但很高兴看到这种模式。如果您想缩短此过程,可以通过为每个其他位置重复经度、纬度和成员作为更多参数来完成相同的操作。这是最后两项的简写示例:

geoadd  va-universities -77.425556 37.242778 virginia-state-university -80.425 37.225 virginia-tech

在内部,这些地理项实际上并没有什么特别的——它们由 Redis 存储为 zset 或排序集。为了说明这一点,让我们在键 va-universities 上运行更多命令:

TYPE va-universities

这会返回 zset,就像任何其他排序集一样。现在,如果我们尝试取回所有值并包含分数,会发生什么?

ZRANGE va-universities 0 -1 WITHSCORES

这将返回上面输入的成员的批量回复,其中包含一个非常大的数字 - 52 位整数。该整数实际上是 geohash 的表示,这是一种巧妙的小结构,可以代表地球上的任何地方。稍后我们将更深入地研究,并且不会以这种方式真正与地理空间数据进行交互,但了解数据的存储方式总是有好处的。

现在我们有了一些数据可以使用,让我们看看 GEODIST 命令。使用此命令,您可以确定之前在同一键下输入的两点之间的距离。因此,我们来计算成员 virginia-techchristopher-newport-university 之间的距离:

GEODIST va-universities virginia-tech christopher-newport-university

这应该输出 349054.2554687438,或两个地点之间的距离(以米为单位)。您还可以提供第三个参数作为单位 mi(英里)、km(公里)、ft(英尺)或 m(米,默认值)。让我们得到以英里为单位的距离:

GEODIST va-universities virginia-tech christopher-newport-university mi

应响应“216.89279795987412”。

在进一步讨论之前,我们先来谈谈为什么计算两个地理空间点之间的距离不仅仅是简单的几何计算。地球是圆的(或接近圆的),因此当您远离赤道时,经线之间的距离开始收敛,并在两极“相遇”。因此,要计算距离,您需要考虑地球。

值得庆幸的是,Redis 使我们免受这种数学计算的影响(如果您感兴趣,这里有一个纯 JavaScript 实现的示例)。需要注意的是,Redis 确实假设地球是一个完美的球体(半正矢公式),并且它可能会引入高达 0.5% 的误差,这对于大多数应用程序来说已经足够了,尤其是对于商店查找器之类的应用程序。

大多数时候,我们需要某个位置特定半径内的所有点,而不仅仅是两点之间的距离。我们可以使用 GEORADIUS 命令来完成此操作。 GEORADIUS 命令至少需要键、经度、纬度、距离和单位。因此,让我们查找数据集中距该点 100 英里范围内的所有大学。

GEORADIUS va-universities -78.245278 37.496111 100 mi

返回结果:

1) "longwood-university"
2) "virginia-state-university"
3) "virginia-commonwealth-university"
4) "university-of-virginia"
5) "university-of-mary-washington"
6) "college-of-william-and-mary"
7) "virginia-military-institute"
8) "james-madison-university”

GEORADIUS 有几个选项。假设我们想要获取指定点与所有位置之间的距离。我们可以通过在末尾添加 WITHDIST 参数来做到这一点:

GEORADIUS va-universities -78.245278 37.496111 100 mi WITHDIST

这会返回包含位置成员和距离(以指定单位表示)的批量回复:

1) 1) "longwood-university"
   2) "16.0072"
2) 1) "virginia-state-university"
   2) "48.3090"
3) 1) "virginia-commonwealth-university"
   2) "43.5549"
4) 1) "university-of-virginia"
   2) "39.0439"
5) 1) "university-of-mary-washington"
   2) "69.7595"
6) 1) "college-of-william-and-mary"
   2) "85.9017"
7) 1) "virginia-military-institute"
   2) "68.4639"
8) 1) "james-madison-university"
   2) “74.1314"

另一个可选参数是 WITHCOORD,正如您可能已经猜到的那样,它会返回经度和纬度坐标。您也可以将其与 WITHDIST 参数混合使用。让我们试试这个:

GEORADIUS va-universities -78.245278 37.496111 100 mi WITHCOORD WITHDIST

结果集变得有点复杂:

1) 1) "longwood-university"
   2) "16.0072"
   3) 1) "-78.395833075046539"
      2) "37.297776773137613"
2) 1) "virginia-state-university"
   2) "48.3090"
   3) 1) "-77.425554692745209"
      2) "37.242778393422277"
3) 1) "virginia-commonwealth-university"
   2) "43.5549"
   3) 1) "-77.453256547451019"
      2) "37.546615418792236"
4) 1) "university-of-virginia"
   2) "39.0439"
   3) 1) "-78.478890359401703"
      2) "38.029999417483971"
5) 1) "university-of-mary-washington"
   2) "69.7595"
   3) 1) "-77.474998533725739"
      2) "38.301944581227126"
6) 1) "college-of-william-and-mary"
   2) "85.9017"
   3) 1) "-76.706942617893219"
      2) "37.27083268721384"
7) 1) "virginia-military-institute"
   2) "68.4639"
   3) 1) "-79.440000951290131"
      2) "37.789999344511962"
8) 1) "james-madison-university"
   2) "74.1314"
   3) 1) "-78.868888914585114"
      2) "38.449445074931383"

请注意,尽管我们的参数顺序相反,但距离位于坐标之前。 Redis 不关心您指定 WITH* 参数的顺序,但它会返回坐标之前的距离。还有一个带有参数 (WITHHASH),但我们将在后面的部分中介绍它 - 只要知道它将出现在您的响应中的最后即可。

关于此处进行的计算的简短说明 - 如果您考虑一下我们之前在 GEODIST 如何工作中介绍的数学,让我们考虑一下半径。由于半径是一个圆,因此我们必须考虑将圆放置在球体上,这与应用在平面上的简单圆有很大不同。再次强调,Redis 为我们完成了所有这些计算(值得庆幸的是)。

现在,我们来介绍一下 GEORADIUSGEORADIUSBYMEMBER 的相关命令。 GEORADIUSBYMEMBER 的工作方式与 GEORADIUS 完全相同,但您可以指定密钥中已有的成员,而不是在参数中指定经度和纬度。例如,这将返回成员 university-of-virginia 100 英里范围内的所有成员。

GEORADIUSBYMEMBER va-universities university-of-virginia 100 mi

您可以在 GEORADIUSBYMEMBER 上使用相同的单位和 WITH* 参数和单位,就像在 GEORADIUS 上一样。

之前,当我们在密钥上运行 ZRANGE 时,您可能想知道如何从使用 GEOADD 添加的位置中获取坐标 - 我们可以完成此操作使用 GEOPOS 命令。通过提供密钥和成员,我们可以取回坐标:

GEOPOS va-universities university-of-virginia

这应该产生以下结果:

1) 1) "-78.478890359401703"
   2) “38.029999417483971"

如果您回顾一下我们添加 university-of-virginia 的值时,会发现数字略有不同,尽管它们四舍五入到相同的数量。这是由于 Redis 以 geohash 格式存储坐标的方式所致。同样,对于大多数应用程序来说,这非常接近且足够好 — 在上面的示例中,GEOPOS 的输入和输出之间的实际距离差为 5.5 英寸/14 厘米。

这将引导我们进入最终的 Redis GEO 命令:GEOHASH。这将返回用于保存坐标的 geohash 值。前面提到,这是一个基于网格的聪明系统,可以用多种方式表示 - Redis 使用 52 位整数,但更常见的表示方式是基数 32 字符串。使用带有键和成员的 GEOHASH 命令,Redis 将返回表示该位置的以 32 为基数的字符串。如果我们运行命令:

GEOHASH va-universities university-of-virginia

你会回来的:

1) "dqb0q5jkv30"

这是 geohash base-32 字符串表示形式。 Geohash 字符串有一个巧妙的属性,如果从字符串右侧删除字符,则会逐渐降低坐标的精度。这可以通过 geohash 网站来说明 - 查看这些链接并查看坐标和地图如何远离原始位置:

  • http://geohash.org/dqb0q5jkv30(非常准确)
  • http://geohash.org/dqb0q5jkv3
  • http://geohash.org/dqb0q5jkv
  • http://geohash.org/dqb0q5jk
  • http://geohash.org/dqb0q5j
  • http://geohash.org/dqb0q5
  • http://geohash.org/dqb0q
  • http://geohash.org/dqb0
  • http://geohash.org/dqb
  • http://geohash.org/dq
  • http://geohash.org/d(非常不准确)

我们还需要介绍一个函数,如果您已经熟悉 Redis 排序集,那么您已经知道它了。由于您的地理空间数据实际上只是存储在 zset 中,因此我们可以使用 ZREM 删除项目:

ZREM va-universities university-of-virginia

商店查找服务器

现在我们已经掌握了使用 Redis GEO 命令的基础知识,接下来让我们构建一个基于 Node.js 的商店查找服务器作为示例。我们将使用上面的数据,所以我想从技术上讲,这是一个大学查找器,而不是商店查找器,但概念是相同的。在开始之前,请确保已安装 Node.js 和 npm。为您的项目创建一个目录,然后在命令行切换到该目录。在命令行中输入:

npm init

这将通过询问您几个问题来创建您的 package.json 文件。初始化项目后,我们将安装四个模块。再次从命令行运行以下四个命令:

npm install express --save
npm install pug --save
npm install redis --save
npm install body-parser --save

第一个模块是 Express.js,一个 Web 服务器模块。为了配合服务器,我们还需要安装模板系统。对于这个项目,我们将使用 pug(正式名称为 Jade)。 Pug 与 Express 很好地集成,使我们能够仅用几行代码创建一个基本的页面模板。我们还安装了node_redis,它管理Node.js 和Redis 服务器之间的连接。最后,我们需要另一个模块来处理 HTTP POST 值的解释:body-parser。

对于我们的第一步,我们只是让服务器能够接受 HTTP 请求并使用值填充模板。

var 
  bodyParser  = require('body-parser'), 
  express     = require('express'),
  
  app = express();
  
app.set('view engine', 'pug'); //this associates the pug module with the res.render function

app.get(  // method "get"
  '/',    // the route, aka "Home"
  function(req, res) {
    res.render('index', { //you can pass any value to the template here
      pageTitle: 'University Finder' 
    });
  }
);

app.post( // method "post"
  '/', 
  bodyParser.urlencoded({ extended : false }), // this allows us to parse the values POST'ed from the form
  function(req,res) {
    var
      latitude  = req.body.latitude,    // req.body contains the post values
      longitude = req.body.longitude;
      
    res.render('index', { 
      pageTitle : 'University Finder Results',
      latitude  : latitude,
      longitude : longitude,
      results   : []                  // we'll populate it later
    });
  }
);

app.listen(3000, function () {
  console.log('Sample store finder running on port 3000.');
});

只有当 HTTP 客户端(也称为浏览器)使用 GET 或 class="inline">POST 请求时,此服务器才会成功提供顶级页面('/')方法。

我们需要一个简单的模板——足以显示标题、表单,并(稍后)显示结果。 Pug 是一种非常简洁的模板语言,具有相关的空格。因此,通过缩进标签嵌套,缩进之后的一行的第一个单词是标签(结束标签由解析器推断),我们使用 #{} 插入值。这需要一些时间来适应,但您可以使用最少的字符创建大量 HTML - 请查看哈巴狗网站以了解更多信息。请注意,在撰写本文时,Pug 官方网站尚未更新。这是有关该问题的官方 GitHub 票证。

//- Anything that starts with "//-" is a non-rendered comment
//- add the doctype for HTML 5
doctype html
//- the HTML tag with the attribute "lang" equal to "en"
html(lang="en")        
  head
    //- this produces a title tag and the "=" means to assign the entire value of pageTitle (passed from our server) between the opening and closing tag
    title= pageTitle
  body
    h1 University Finder
    form(action="/" method="post")
      div 
        label(for="#latitude") Latitude  
        //- "value=" will pull in the 'latitude' variable in from the server, ignoring it if the variable doesn't exist
        input#latitude(type="text" name="latitude" value= latitude)
      div
        label(for="#longitude") Longitude  
        input#longitude(type="text" name="longitude" value= longitude)
      button(type="submit") Find
    //- "if" is a reserved word in Pug - anything that follows and is indented one more level will only be rendered if the 'results' variable is present
    if results
      h2 Showing Results for #{latitude}, #{longitude}

我们可以通过在命令行启动服务器来尝试我们的商店查找器:

node app.js

然后将浏览器指向 http://localhost:3000/

您应该看到一个普通的、无样式的页面,其中有一个大标题,上面写着“University Finder”,以及一个带有几个文本框的表单。由于浏览器的正常页面请求是 GET 请求,因此该页面是由app.get 参数中的函数。

使用Node.js和Redis构建一个商店查找器

如果您在纬度和经度教科书中输入值并单击“查找”,您将看到这些结果已呈现并显示在“显示...的结果”行上。此时,您将无法获得任何结果,因为我们还没有真正集成 Redis。

使用Node.js和Redis构建一个商店查找器

集成Redis

要集成 Redis,首先我们需要进行一些设置。在变量声明中,包含客户端的模块和变量(尚未定义)。

    ...
    redis       = require('redis'),
    client,
    ...

变量声明后,我们需要创建与 Redis 的连接。在我们的示例中,我们将假设在默认端口上进行本地主机连接,并且无需进行身份验证(在生产环境中,请确保保护您的 Redis 服务器)。

client = redis.createClient();

node_redis 的一个巧妙功能是,客户端会在建立连接时将命令排队,因此无需担心等待与 Redis 服务器建立连接。

现在我们的节点实例有一个可以接受连接的 Redis 客户端,让我们开始研究商店查找器的核心部分。我们将获取用户的纬度和经度并将其应用到 GEORADIUS 命令。我们的示例使用 100 英里半径。我们还想要获得这些结果的距离和坐标。

在回调中,我们会处理出现的任何错误。如果没有发现错误,则映射结果以使它们更有意义并且更容易集成到模板中。然后将这些结果输入到模板中。

app.post( // method "post"
  '/', 
  bodyParser.urlencoded({ extended : false }), // this allows us to parse the values POST'ed from the form
  function(req,res,next) {
    var
      latitude  = req.body.latitude,    // req.body contains the post values
      longitude = req.body.longitude;
 
     client.georadius(
      'va-universities',    //va-universities is the key where our geo data is stored
      longitude,            //the longitude from the user
      latitude,             //the latitude from the user
      '100',                //radius value
      'mi',                 //radius unit (in this case, Miles)
      'WITHCOORD',          //include the coordinates in the result
      'WITHDIST',           //include the distance from the supplied latitude & longitude
      'ASC',                //sort with closest first
      function(err, results) {
        if (err) { next(err); } else { //if there is an error, we'll give it back to the user
          //the results are in a funny nested array. Example:
          //1) "longwood-university"        [0]
          //2) "16.0072"                    [1]
          //3)  1) "-78.395833075046539"    [2][0]
          //    2) "37.297776773137613"     [2][1]
          //by using the `map` function we'll turn it into a collection (array of objects)
          results = results.map(function(aResult) {
            var
              resultObject = {
                key       : aResult[0],
                distance  : aResult[1],
                longitude : aResult[2][0],
                latitude  : aResult[2][1]
              };
              
            return resultObject;
          })
          res.render('index', { 
            pageTitle : 'University Finder Results',
            latitude  : latitude,
            longitude : longitude,
            results   : results
          });
        }
      }
    );
    
  }
);

在模板中,我们需要处理结果集。 Pug 对数组有无缝迭代(几乎是口头语法)。这是将这些值引入单个结果的问题;模板将处理其他所有事情。

each result in results
    div
      h3 #{result.key}
      div
        strong Distance: 
        | #{result.distance}
        |  miles
      div
        strong Coordinates: 
        | #{result.latitude}
        | , 
        | #{result.longitude}
        | (
        a(href="https://www.openstreetmap.org/#map=18/"+result.latitude+"/"+result.longitude) Map
        | )

准备好最终模板和节点代码后,再次启动 app.js 服务器并将浏览器指向 http://localhost:3000/。

如果您在框中输入纬度 38.904722 和经度 -77.016389(弗吉尼亚州北边界华盛顿特区的坐标)并单击“查找”,您将得到三个结果。如果您将这些值更改为纬度 37.533333 和经度 -77.466667(弗吉尼亚州里士满、州首府和该州中/东部),您将看到 10 个结果。

使用Node.js和Redis构建一个商店查找器

此时,您已经有了商店查找器的基本部分,但您需要对其进行调整以适合您的需求自己的项目。

  • 大多数用户不会考虑坐标,因此您需要考虑一种更加用户友好的方法,例如:

    1.使用客户端 JavaScript 通过 Geolocation API 检测位置

    2.使用基于 IP 的地理定位服务

    3.询问用户邮政编码或地址,并使用地理编码服务将其转换为坐标。市场上有许多不同的地理编码服务,因此请选择最适合您的目标区域的服务。

  • 此脚本不进行表单验证。如果您保留纬度和经度输入框,您需要确保验证数据并避免出现错误消息。

  • 将位置键扩展为更有用的信息。如果您使用 Redis 存储有关每个位置的更多信息,请考虑将该信息存储在哈希中,其键与从 GEORADIUS 返回的成员相匹配。您需要对 Redis 进行额外的调用。

  • 与 Google 地图、OpenStreetMap 或 Bing 地图等地图服务更紧密地集成,以提供嵌入式地图和路线。