访问任何连锁店或商店的网站,您可能会找到一个“商店查找器”:一个看似简单的小页面,您可以在其中输入您的地址或邮政编码/邮政编码,并提供您附近的位置。作为客户,这很棒,因为您可以找到最接近的东西,并且业务影响是显而易见的。
构建“商店查找器”实际上是一项具有挑战性的任务。在本教程中,我们将介绍如何在 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 服务器,让我们浏览一下地理命令。Redis 有六个与地理空间索引直接相关的命令:GEOADD、GEOHASH、GEOPOS、GEODIST、GEORADIUS和GEORADIUSBYMEMBER。
让我们从GEOADD. 正如您可能想象的那样,此命令添加了一个地理空间项目。它有四个require d 参数:键、经度、纬度和成员。键就像一个分组,代表键空间中的单个值。经度和纬度显然是浮点数的坐标;请注意这些值的顺序,因为它们可能与您过去看到的相反。最后,“会员”是您识别位置的方式。在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 或排序集。为了展示这一点,让我们在 key 上运行更多命令 va-universities:
TYPE va-universities
这个,返回zset,就像任何其他排序集一样。现在,如果我们尝试取回所有值并包含分数会发生什么?
ZRANGE va-universities 0 -1 WITHSCORES
这将返回上面输入的成员的批量回复,其中包含一个非常大的数字——一个 52 位整数。整数实际上是 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命令来做到这一点。该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 为我们完成了所有这些计算(谢天谢地)。
GEORADIUS现在,让我们介绍一个与,相关的命令GEORADIUSBYMEMBER。GEORADIUSBYMEMBER工作原理与 完全相同GEORADIUS,但您可以指定密钥中已有的成员,而不是在参数中指定经度和纬度。因此,例如,这将返回成员 100 英里内的所有成员university-of-virginia。
GEORADIUSBYMEMBER va-universities university-of-virginia 100 mi
您可以在 上使用相同的单位、WITH*参数和单位。GEORADIUSBYMEMBERGEORADIUS
早些时候,当我们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 位整数,但更常见的表示是 base-32 字符串。使用GEOHASH带有键和成员的命令,Redis 将返回表示该位置的 base-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 的 store finder 服务器作为示例。我们将使用上面的数据,所以我想这在技术上是 大学搜索而不是商店搜索,但概念是相同的。在开始之前,请确保已安装 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 与 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.'); });
GET此服务器仅在 HTTP 客户端(也称为浏览器)使用orPOST方法请求时才能成功提供顶级页面('/') 。
我们将需要一个简单的模板——足以显示标题、表单和(稍后)显示结果。Pug 是一种非常简洁的模板语言,带有相关的空格。因此,使用缩进标签嵌套,缩进后一行的第一个单词是标签(解析器推断出结束标签),我们用 #{}. 这需要一些时间来适应,但您可以使用最少的字符创建大量html——查看pug 网站了解更多信息。请注意,在撰写本文时,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}
我们可以通过在命令行启动服务器来试用我们的 store finder:
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 客户端,让我们在我们的 store finder 的核心上工作。我们将获取用户的纬度和经度并将其应用于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(弗吉尼亚州里士满,州首府和州中部/东部),您将看到十个结果。
至此,您已经拥有了商店查找器的基本部分,但您需要对其进行调整以适合您自己的项目。
大多数用户不考虑坐标,因此您需要考虑更用户友好的方法,例如:
1. 使用客户端JavaScript使用Geolocation api检测位置
2. 使用基于 IP 的地理定位器服务
3. 询问用户邮政编码或地址,并使用地理编码服务将两者转换为坐标。市场上有许多不同的地理编码服务,因此请选择一种适合您的目标区域的服务。将位置键扩展为更多有用的信息。如果您使用 Redis 存储有关每个位置的更多信息,请考虑将该信息存储在哈希中,并使用与您从GEORADIUS. 您需要对 Redis 进行额外的调用。
与Google Maps、OpenStreetMap或Bing Maps等地图服务更紧密地集成,以提供嵌入式地图和方向。