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

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

构造“商店查找器”实际上是一项艰巨的任务。 在本教程中,我们将介绍如何在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'

在继续操作之前,您需要下载 ,构建和安装支持geo命令的Redis版本。

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

让我们从GEOADD开始。 您可能会想到,此命令将添加地理空间项。 它具有四个必需的参数:key,经度,纬度和成员。 键就像一个分组,代表键空间中的单个值。 经度和纬度显然是浮点数的坐标; 请注意这些值的顺序,因为它们可能与您惯常看到的相反。 最后,“成员”是您要识别位置的方式。 在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的表示, geohash是一个聪明的小结构,可以表示地球上的任何位置。 稍后我们将进行更深入的探讨,并且不会真正以这种方式与地理空间数据进行交互,但是了解您的数据存储方式总是很高兴的。

现在我们有了一些数据要处理,让我们看一下GEODIST命令。 使用此命令,您可以确定先前在同一键下输入的两点之间的距离。 因此,让我们找到virginia-tech成员和christopher-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确实假设地球是一个完美的球体(Haversine公式),并且可能会引入高达0.5%的误差,这对于大多数应用程序来说已经足够了,特别是对于商店查找器而言。

大多数时候,我们会希望所有点都在某个位置的一定半径内,而不仅仅是两个点之间的距离。 我们可以使用GEORADIUS命令执行此GEORADIUSGEORADIUS命令至少期望键,经度,纬度,距离和单位。 因此,让我们在此点外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工作原理中涉及的数学,那么GEODIST考虑一下半径。 由于半径是一个圆,因此我们必须考虑将一个圆放置在球体上,这与在平面上应用一个简单的圆完全不同。 再次,Redis为我们做了所有这些计算(非常感谢)。

现在,让我们介绍与GEORADIUS相关的命令, GEORADIUSBYMEMBERGEORADIUSBYMEMBER作品完全一样的GEORADIUS ,但不是指定一个经度,并在参数的纬度,你可以在你的钥匙已经指定一个成员。 因此,例如,这将使所有会员返回距university-of-virginia会员100英里范围内的所有会员。

GEORADIUSBYMEMBER va-universities university-of-virginia 100 mi

您可以使用相同的单位和WITH*参数和单位对GEORADIUSBYMEMBER ,你可以在GEORADIUS

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

GEOPOS va-universities university-of-virginia

这应该产生以下结果:

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

如果回头看一下我们为university-of-virginia添加的值,则数字略有不同,尽管它们取整为相同的数量。 这是由于Redis如何以geohash格式存储坐标。 同样,对于大多数应用而言,这非常接近且足够好-在上面的示例中, GEOPOS的输入和输出之间的实际距离差为5.5英寸/ 14厘米。

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

GEOHASH va-universities university-of-virginia

您会回来的:

1) "dqb0q5jkv30"

这是geohash以32为基的字符串表示形式。 Geohash字符串具有一个整洁的属性,如果您从字符串的右侧删除字符,则会逐渐降低坐标的精度。 这可以在geohash网站上进行说明-查看以下链接,查看坐标和地图如何远离原始位置:

我们还需要介绍另外一项功能,如果您已经熟悉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

第一个模块是Web服务器模块Express.js 。 要与服务器一起使用,我们还需要安装一个模板系统。 在这个项目中,我们将使用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客户端(即浏览器)使用GETPOST方法请求时,此服务器才可以成功提供顶层页面('/')。

我们将需要一个简单的模板-正好足以显示标题,表单和(然后)显示结果。 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的参数。

基本表格截图

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

单击屏幕快照后的表格

整合Redis

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

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

在变量声明之后,我们需要创建与Redis的连接。 在我们的示例中,我们将在默认端口上使用localhost连接,并且不进行身份验证(在生产环境中,请确保保护您的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个结果。


至此,您已经具备了商店查找器的基本组成部分,但是您需要对其进行调整以适合您自己的项目。

  • 大多数用户不会考虑坐标,因此您需要考虑一种更加用户友好的方法,例如:
    1.使用客户端JavaScript通过Geolocation API检测位置
    2.使用基于IP的地理定位器服务
    3.向用户询问邮政编码或地址,并使用将其转换为坐标的地理编码服务。 市场上有许多不同的地理编码服务,因此请选择一种对您的目标区域有效的地理编码服务。

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

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

  • Google MapsOpenStreetMapBing Maps地图服务更紧密地集成,以提供嵌入式地图和方向。

翻译自: https://code.tutsplus.com/tutorials/building-a-store-finder-with-nodejs-and-redis--cms-26283

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值