空间过程

空间过程为您的数据提供了地理位置功能,是对 Neo4j 自带的 空间函数 的补充。更多扩展的空间功能可以在 Neo4j 空间库 中找到。

限定名称 类型

apoc.spatial.geocode
apoc.spatial.geocode(location STRING, maxResults INTEGER, quotaException BOOLEAN, config MAP<STRING, ANY>)) - 使用地理编码服务(默认:OpenStreetMap)返回给定地址的地理位置(纬度、经度和描述)。

过程

apoc.spatial.reverseGeocode
apoc.spatial.reverseGeocode(latitude FLOAT, longitude FLOAT, quotaException BOOLEAN, config MAP<STRING, ANY>) - 使用地理编码服务(默认:OpenStreetMap)从给定的地理位置(纬度、经度)返回文本地址。此过程最多返回一个结果。

过程

apoc.spatial.sortByDistance
apoc.spatial.sortByDistance(paths LIST<PATH>) - 根据 NODE 值中的纬度/经度值,按距离之和对给定的 PATH 值集合进行排序。

过程

地理编码 (Geocode)

geocode 过程将文本地址转换为包含 纬度 (latitude)经度 (longitude)描述 (description) 的位置信息。尽管这只是一个单一的功能,但结合内置的 pointdistance 函数,我们可以获得非常强大的结果。

首先,我们如何使用这个过程

CALL apoc.spatial.geocodeOnce('21 rue Paul Bellamy 44000 NANTES FRANCE')
YIELD location
RETURN location.latitude, location.longitude
表 1. 结果
location.latitude location.longitude

47.2221667

-1.5566625

该过程有三种形式

  • geocodeOnce(address) 返回零个或一个结果。

  • geocode(address,maxResults) 返回零个、一个或最多 maxResults 个结果。

  • reverseGeocode(latitude,longitude) 返回零个或一个结果。

这是因为后台使用的地理编码服务(OSM、Google、OpenCage 或其他)可能对同一个查询返回多个结果。GeocodeOnce() 旨在返回第一个或排名最高的结果。

第三个过程 reverseGeocode 会将包含 纬度经度 的位置转换为文本地址。

CALL apoc.spatial.reverseGeocode(47.2221667,-1.5566625) YIELD location
RETURN location.description;
表 2. 结果
location.description

"21, Rue Paul Bellamy, Talensac - Pont Morand, Hauts-Pavés - Saint-Félix, Nantes, Loire-Atlantique, Pays de la Loire, France métropolitaine, 44000, France"

配置地理编码

有一些选项可以在 apoc.conf 文件中设置,或者通过 $config 参数(请参阅下文的 通过配置参数映射进行配置 章节)来控制服务。

apoc.conf 中,我们可以传入

  • apoc.spatial.geocode.provider=osm (osm, google, opencage 等)

  • apoc.spatial.geocode.osm.throttle=5000 (查询之间的延迟毫秒数,以避免使 OSM 服务器过载)

  • apoc.spatial.geocode.google.throttle=1 (查询之间的延迟毫秒数,以避免使 Google 服务器过载)

  • apoc.spatial.geocode.google.key=xxxx (用于 Google 地理编码访问的 API 密钥)

  • apoc.spatial.geocode.google.client=xxxx (用于 Google 地理编码访问的客户端代码)

  • apoc.spatial.geocode.google.signature=xxxx (用于 Google 地理编码访问的客户端签名)

对于 Google,您应该使用密钥或客户端代码与签名的组合。有关详细信息,请阅读 Google 地理编码访问页面:https://developers.google.com/maps/documentation/geocoding/get-api-key#key

配置自定义地理编码提供程序

地理编码

对于任何非 'osm' 或 'google' 的提供程序,您可以获得一个可配置的供应器,它需要两个额外的设置:'url' 和 'key'。'url' 必须包含 'PLACE' 和 'KEY' 这两个词。'KEY' 将被替换为您在注册服务时从提供商处获得的密钥。当调用该过程时,'PLACE' 将被替换为要进行地理编码的地址。

反向地理编码

'url' 必须包含 'LAT'、'LNG' 和 'KEY' 这三个词。当调用该过程时,'LAT' 将被替换为纬度,'LNG' 将被替换为要进行反向地理编码的经度。

例如,要使服务与 OpenCage 一起工作,请执行以下步骤

apoc.spatial.geocode.provider=opencage
apoc.spatial.geocode.opencage.key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
apoc.spatial.geocode.opencage.url=http://api.opencagedata.com/geocode/v1/json?q=PLACE&key=KEY
apoc.spatial.geocode.opencage.reverse.url=http://api.opencagedata.com/geocode/v1/json?q=LAT+LNG&key=KEY
  • 确保上面的 'XXXXXXX' 部分已被替换为您实际的密钥

  • 重新启动 Neo4j 服务器,然后测试地理编码过程以查看它们是否工作

通过配置参数映射进行配置

或者,我们可以传递一个配置映射 (config map)。
请注意,这些配置的优先级高于 apoc.conf 中的设置。
我们可以传递一个提供程序键,它等同于 apoc.spatial.geocode.provider 设置键,其他键等同于 apoc.spatial.geocode.<PROVIDER>.<KEY> 设置。

例如:

CALL apoc.spatial.geocodeOnce('<MY_PLACE>', {
  provider: 'opencage',
  url: 'http://api.opencagedata.com/geocode/v1/json?q=PLACE&key=KEY',
  reverseUrl: 'http://api.opencagedata.com/geocode/v1/json?q=LAT+LNG&key=KEY',
  key: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
})

等同于这些(请注意,我们将 UpperCamelCase 键转换为 dot.case,例如从 reverseUrl 变为 reverse.url

apoc.spatial.geocode.provider=opencage
apoc.spatial.geocode.opencage.key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
apoc.spatial.geocode.opencage.url=http://api.opencagedata.com/geocode/v1/json?q=PLACE&key=KEY
apoc.spatial.geocode.opencage.reverse.url=http://api.opencagedata.com/geocode/v1/json?q=LAT+LNG&key=KEY

如果我们不通过配置映射传递提供程序,将选择 apoc.spatial.geocode.provider 设置,否则默认为 'osm'。例如

/* apoc.conf
  ...
  apoc.spatial.geocode.provider=google
  ...
*/
CALL apoc.spatial.geocodeOnce('<MY_PLACE>', {key: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'})

将传递一个类似 apoc.spatial.geocode.google.key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 的配置。

在更大的 Cypher 查询中使用地理编码

一个更复杂或更有用的示例,对节点属性中找到的地址进行地理编码

MATCH (a:Place)
WHERE a.address IS NOT NULL
CALL apoc.spatial.geocodeOnce(a.address) YIELD location
RETURN location.latitude AS latitude, location.longitude AS longitude, location.description AS description

计算位置之间的距离

如果我们希望计算地址之间的距离,我们需要使用 point() 函数将纬度和经度转换为 Cypher Point 类型,然后使用 point.distance() 函数来计算距离

WITH point({latitude: 48.8582532, longitude: 2.294287}) AS eiffel
MATCH (a:Place)
WHERE a.address IS NOT NULL
CALL apoc.spatial.geocodeOnce(a.address) YIELD location
WITH location, point.distance(point(location), eiffel) AS distance
WHERE distance < 5000
RETURN location.description AS description, distance
ORDER BY distance
LIMIT 100

按距离排序 (sortByDistance)

第二个过程使您能够根据节点上的纬度/经度属性,按距离总和对给定的 PATH 值集合进行排序。

示例数据

CREATE (bruges:City {name:"bruges", latitude: 51.2605829, longitude: 3.0817189})
CREATE (brussels:City {name:"brussels", latitude: 50.854954, longitude: 4.3051786})
CREATE (paris:City {name:"paris", latitude: 48.8588376, longitude: 2.2773455})
CREATE (dresden:City {name:"dresden", latitude: 51.0767496, longitude: 13.6321595})
MERGE (bruges)-[:NEXT]->(brussels)
MERGE (brussels)-[:NEXT]->(dresden)
MERGE (brussels)-[:NEXT]->(paris)
MERGE (bruges)-[:NEXT]->(paris)
MERGE (paris)-[:NEXT]->(dresden)

查找路径并按距离排序

MATCH (a:City {name:'bruges'}), (b:City {name:'dresden'})
MATCH p=(a)-[*]->(b)
WITH collect(p) as paths
CALL apoc.spatial.sortByDistance(paths) YIELD path, distance
RETURN path, distance

图重构

为了避免在多个查询中重复对同一内容进行地理编码(特别是如果数据库将由许多人使用),将结果持久化到数据库中是一个好主意,这样后续的调用可以使用已保存的结果。

地理编码并持久化结果

MATCH (a:Place)
WHERE a.address IS NOT NULL AND a.latitude IS NULL
WITH a LIMIT 1000
CALL apoc.spatial.geocodeOnce(a.address) YIELD location
SET a.latitude = location.latitude
SET a.longitude = location.longitude

请注意,上述命令仅对前 1000 个尚未进行地理编码的“地点”节点进行地理编码。此查询可以多次运行,直到所有地点都完成地理编码。为什么要这样做?有两个很好的理由:

  • 地理编码服务是一项公共服务,可能会限制或封禁请求过于频繁的站点,因此控制我们的请求量非常有用。

  • 事务正在更新数据库,明智的做法是不要在同一个事务中更新太多内容,以避免耗尽内存。这个技巧将保持内存使用量非常低。

现在在距离查询中使用这些结果

WITH point({latitude: 48.8582532, longitude: 2.294287}) AS eiffel
MATCH (a:Place)
WHERE a.latitude IS NOT NULL AND a.longitude IS NOT NULL
WITH a, point.distance(point(a), eiffel) AS distance
WHERE distance < 5000
RETURN a.name, distance
ORDER BY distance
LIMIT 100

结合空间和日期时间函数可以实现更复杂的查询

WITH point({latitude: 48.8582532, longitude: 2.294287}) AS eiffel
MATCH (e:Event)
WHERE e.address IS NOT NULL AND e.datetime IS NOT NULL
CALL apoc.spatial.geocodeOnce(e.address) YIELD location
WITH e, location,
distance(point(location), eiffel) AS distance,
            (apoc.date.parse('2016-06-01 00:00:00','h') - apoc.date.parse(e.datetime,'h'))/24.0 AS days_before_due
WHERE distance < 5000 AND days_before_due < 14 AND apoc.date.parse(e.datetime,'h') < apoc.date.parse('2016-06-01 00:00:00','h')
RETURN e.name AS event, e.datetime AS date,
location.description AS description, distance
ORDER BY distance