GraphGists

想法

这项工作的目的是展示,利用从公共机构获取的开放数据,可以构建一个有趣的服务(至少在我看来是这样:))。这个示例服务方便了旅行,并使用了由联合国欧洲经济委员会 (UNECE) 提供的关于国家和城市的公共数据,同时还补充了从OpenStreetMap 获取的关于餐馆和旅馆的信息。如今,即使你不是很富有,也可以负担得起环游世界——有廉价航空公司,你可以免费睡在别人家的沙发上,在网上找到大量关于如何看世界而不至于破产的有用技巧。现如今,问题可能在于去哪里度过一个周末、一次短途旅行或一次一生难忘的冒险,因为选择的可能性非常巨大。旅行助手服务旨在为人们推荐旅行方向和设施。

数据

为了实现上述服务的基本功能,需要以下数据:

  • 有关不同地点的数据:国家、地区、城市——在本例中,开放数据从联合国欧洲经济委员会 (UNECE) 获取。

  • POI 数据:可以从OpenStreetMap 获取的酒店和餐馆。这些数据在开放数据库许可下可用,并且是© OpenStreetMap 贡献者的财产。

  • 所有这些都应该补充有关人员、他们到各地旅行以及参观餐馆、酒吧的信息。我相信这些缺失的数据可以由旅行助手服务自己收集。

数据模型

设计的数据模型如下图所示:

fSZBomW

模型中的节点和关系以及相应的示例显示在下表中:

表 1. 域
属性 示例

姓名, 年龄, 博客地址

Bill, 32 岁

地点:国家

名称, 代码

波兰

地点:地区

名称, 代码, 类型

普罗旺斯 (法国)

地点:城市

名称, 代码, 坐标

华沙

住宿地

名称, 网站, 地址

纽约希尔顿酒店, 苏格兰高地营地

餐饮场所

名称, 网站, 地址, 菜系

巴黎薄饼店, 芝加哥汉堡店

旅行

类型, 时长, 年份季节

欧洲之旅, 伦敦周末游

表 2. 关系
起始节点 关系 结束节点 示例

地点

BELONGS_TO

地点

华沙 BELONGS_TO 波兰

LIVES_IN (从, 到)

地点

Kate LIVES_IN 莫斯科 (从 2014)

WENT_FOR

旅行

Bob WENT_FOR 环游世界

旅行

TO (交通方式)

地点

旅行 TO 伦敦 (交通方式 = 飞机)

旅行

STARTS_FROM

地点

伦敦之旅 STARTS_FROM 柏林

旅行

IS_PART_OF (顺序号)

旅行

伦敦之旅 IS_PART_OF 环游世界 (顺序号 = 1)

旅行

STAYED_AT (评分, 每晚平均价格)

住宿地

伦敦之旅期间 Kate STAYED at 希尔顿酒店 (评分 = 5, 每晚平均价格 = 1000)

旅行

WENT_TO (评分, 平均花费)

餐饮场所

伦敦之旅期间 Kate WENT_TO at Dawsan 餐馆 (评分 = 5, 平均花费 = 1000)

餐饮场所

IS_LOCATED_IN

地点

Dawsan 餐馆 IS_LOCATED_IN 伦敦

住宿地

IS_LOCATED_IN

地点

希尔顿酒店 IS_LOCATED_IN 伦敦

图数据上传

首先,将测试数据添加到数据库。

上传的数据包含人员、地点以及这些人员到各地旅行的信息。

地点

  • 波兰 : 华沙, 克拉科夫, 扎科帕内, 托伦, 格但斯克, 波兹南;

  • 法国 : 巴黎, 尼斯, 阿维尼翁, 里昂, 马赛, 佩皮尼昂;

  • 意大利 : 罗马, 米兰, 巴勒莫, 那不勒斯, 巴里

  • 西班牙 : 巴塞罗那, 马德里, 塞维利亚, 毕尔巴鄂

  • 葡萄牙 : 波尔图, 里斯本, 卡斯凯什, 法鲁

  • 英国: 伦敦, 格拉斯哥, 曼彻斯特, 加的夫

  • 美国 : 芝加哥, 纽约, 波士顿, 费城, 华盛顿, 西雅图, 旧金山, 圣何塞, 蒙特雷, 圣巴巴拉, 洛杉矶, 拉斯维加斯

这些地点的数据来自 UNECE 来源。包含国家、地区和城市的 CSV 文件已修改,仅包含上述国家、城市和地区的数据。这样,就可以在此处进行图渲染。数据以上述方式上传。

create index on :Place(name);
create index on :Country(name);
create index on :Region(name);
create index on :City(name);
create index on :Place(code);
create index on :Country(code);
create index on :Region(code);
create index on :City(code);

load csv with headers from
'https://gist.githubusercontent.com/justynaGithub/45be86f418c009f0dcaf/raw/f8666bcc7cd9a9e8a0e191f148c67b88b6b58d06/countries.csv' as line fieldterminator ','
WITH line.CountryCode as CountryCode, line.CountryName as CountryName
CREATE (p:Place:Country{code:CountryCode, name:CountryName});

load csv with headers from
'https://gist.githubusercontent.com/justynaGithub/ce3bc36eb55c71a7931a/raw/fd9962071e13b1db7ace1cb2b971c150c91cda50/subdiv.csv' as line fieldterminator ','
WITH line.CountryCode as CountryCode, line.RegionCode as RegionCode, line.RegionName as RegionName, line.RegionType as RegionType
MATCH (country:Country {code:CountryCode})
CREATE (p:Place:Region{code:RegionCode, name:RegionName})-[:BELONGS_TO]->country;

load csv with headers from
'https://gist.githubusercontent.com/justynaGithub/d7708b8cd2891f876199/raw/e4a64ab07772452b9a23f48adbab16dd7213d522/cities.csv' as line fieldterminator ','
WITH line.CountryCode as CountryCode, line.CityCode as CityCode, line.CityNameNoSpecialChars as CityName, line.RegionCode as RegionCode, line.Coordinates as Coordinates
MATCH (country:Country {code:CountryCode})
OPTIONAL MATCH country<-[:BELONGS_TO]-(region:Region{code:RegionCode})
FOREACH (o IN CASE WHEN region IS NOT NULL THEN [region] ELSE [] END |
	CREATE (c:Place:City{code:CityCode, name:CityName, coordinates:Coordinates})-[:BELONGS_TO]->(region)
)
FOREACH (o IN CASE WHEN region IS NULL THEN [region] ELSE [] END |
	CREATE (c:Place:City{code:CityCode, name:CityName, coordinates:Coordinates})-[:BELONGS_TO]->(country)
);

现在,已经有了所选测试国家的图,其中标识了所属的城市和地区。

下一步是上传华沙的餐馆和酒店数据——只选择了这个城市来展示这些数据的应用。来自 OpenStreetMap 的数据是使用 https://overpass-turbo.eu/s/e6d 获取的,并已转换为 CVS 文件。

//restaurants
load csv with headers from
'https://gist.githubusercontent.com/justynaGithub/a5fdb93fc28988d03eb8/raw/554fd7f02a5e57b819533bbb618e0774c6a1755b/restaurantsWarsaw.csv' as line fieldterminator ','
WITH line.name as Name, line.lon as Lon, line.lat as Lat, line.cuisine as Cuisine, line.addr_city as City, line.addr_treet as Street, line.addr_housenumber as Housenumber, line.website as Website
MATCH (warsaw:City{name:'Warszawa'})
CREATE (:Sustenance:Restaurant{name:Name, lon:Lon, lat:Lat,city: City,street:Street, housenumber:Housenumber, cuisine:Cuisine, website:Website})-[:IS_LOCATED_IN]->(warsaw);

//hotels
load csv with headers from
'https://gist.githubusercontent.com/justynaGithub/ee34f74812779b2b692d/raw/2509cd53639b209987a26590cf776ee563679d57/hotelsWarsaw.csv' as line fieldterminator ','
WITH line.name as Name, line.lon as Lon, line.lat as Lat, line.addr_city as City, line.addr_street as Street, line.addr_housenumber as Housenumber, line.website as Website
MATCH (warsaw:City{name:'Warszawa'})
CREATE (:PlaceToSleep:Hotel{name:Name, lon:Lon, lat:Lat, city:City, street:Street, housenumber:Housenumber, website:Website})-[:IS_LOCATED_IN]->warsaw

华沙的餐馆和酒店

MATCH (a)-[r:IS_LOCATED_IN]->(warsaw:City{name:'Warszawa'})
RETURN a, r, warsaw

接下来是添加一些示例人物以及他们到各地的旅行。

人物

  • Kate, 年龄: 30, 来自西班牙马德里, 曾在美国旅行, 去了巴塞罗那

  • Ben, 年龄: 56, 来自英国伦敦, 去了美国

  • Tom, 年龄: 40, 来自西班牙马德里, 在伦敦度过了一个周末

  • John, 年龄: 34, 来自西班牙马德里, 在巴塞罗那度过了一个周末

  • Claudia, 年龄: 26, 来自葡萄牙里斯本, 曾环游波兰

  • Norah, 年龄: 18, 来自美国芝加哥, 曾环游波兰

  • Lucas, 年龄: 30, 来自波兰华沙, 曾环游欧洲

  • Pedro, 年龄: 32, 来自意大利罗马, 曾环游波兰

  • Pierre, 年龄: 40, 来自法国尼斯, 曾环游波兰

  • Laura, 年龄: 31, 来自西班牙马德里, 正在寻找旅行灵感

用例

收集到有关人员及其到各地旅行的数据后,可以使用这些数据来推荐更适合人们需求的地点和设施。示例用例可分为两组:提前规划假期时使用旅行助手,以及在假期中需要时使用旅行助手。下面示例中假设正在寻求帮助的人是 Laura,年龄 31 岁,来自西班牙马德里。

1. 如何使用旅行助手规划假期?

1a. 我是 Laura,31 岁,来自马德里。周末可以去哪里?

MATCH (weekend:Trip{duration:2})-[:STARTS_FROM]->(madrid:Place{name:'Madrid'}),
(trip:Trip)-[:IS_PART_OF]->(weekend),
(trip)-[:TO]->(place:Place)
WHERE place.name <> 'Madrid'
WITH place.name as placeName, count(place) as counts
RETURN placeName
ORDER BY counts DESC

1b. 我是 Laura,31 岁,来自马德里。我计划去美国一个月,想尽可能多地看看地方。告诉我人们是如何在那里旅行的。

MATCH (shortTrip:Trip)-[:TO]->(:Place)-[:BELONGS_TO*]->(:Country{code:'US'}),
(shortTrip)-[:IS_PART_OF]->(usaTrip:Trip)-[:STARTS_FROM]->(start_place:Place)
WHERE usaTrip.duration<32
WITH DISTINCT usaTrip, start_place.name as start_place
MATCH (:Country{code:'US'})<-[:BELONGS_TO*]-(city:Place)<-[to:TO]-(shortTrip:Trip)-[part:IS_PART_OF]->(usaTrip)
WITH usaTrip.name as tripName, start_place, city.name as name, part.order_no as order_no, to.transportation as by
ORDER BY order_no
WITH tripName, start_place, collect({order_no:order_no, to:name, by:by}) as cities
WITH tripName, start_place, cities, size(cities) as nbrOfCities
RETURN tripName, start_place, cities
ORDER BY nbrOfCities DESC

1c. 我是 Laura,31 岁,来自马德里。我需要一次长途旅行的灵感。我想尽可能多地看看地方。向我展示其他人的旅行经历。

MATCH (:Trip)-[:IS_PART_OF]->(longTrip:Trip)-[:STARTS_FROM]->(start_place:Place)
WITH DISTINCT longTrip, start_place.name as start_place
MATCH (city:Place)<-[to:TO]-(shortTrip:Trip)-[part:IS_PART_OF]->(longTrip)
WITH longTrip.name as tripName, start_place, city.name as name, part.order_no as order_no, to.transportation as by
ORDER BY order_no
WITH tripName, start_place, collect({order_no:order_no, to:name, by:by}) as cities
WITH tripName, start_place, cities, size(cities) as nbrOfCities
RETURN tripName, start_place, cities
ORDER BY nbrOfCities DESC

2. 如何在假期期间临时使用旅行助手?

MATCH (restaurant:Sustenance)-[IS_LOCATED_IN]->(:Place{name:'Warszawa'}),
(client:Person)-[:WENT_FOR]->(:Trip)-[meal:WENT_TO]->restaurant
WHERE client.age>25 AND client.age<36
WITH DISTINCT restaurant.name as resto, collect(meal) as meals
WITH resto, (reduce(s = 0 , x IN meals | s + x.rate))/size(meals) as avg_rate
RETURN resto, avg_rate
ORDER BY avg_rate DESC
MATCH (hotel:PlaceToSleep)-[IS_LOCATED_IN]->(:Place{name:'Warszawa'}),
(client:Person)-[:WENT_FOR]->(:Trip)-[stay:STAYED_AT]->hotel
WITH DISTINCT hotel.name as hotel, hotel.website as website, collect(stay) as stays
WITH hotel, website, (reduce(s = 0 , x IN stays | s + x.avg_price_per_night))/size(stays) as avg_price
WHERE avg_price<200
RETURN hotel, website, avg_price
ORDER BY avg_price

2c. 我是 Laura,31 岁,来自马德里。目前正在波兰华沙旅游。我想在波兰待得比原计划更久。接下来我可以去哪里?

MATCH (warsawTrip:Trip)-[:TO]->(place:Place{name:'Warszawa'}),
(warsawTrip)-[warsawPart:IS_PART_OF]->(longTrip:Trip),
(previousPlace:Place)<-[:TO]-(previousTrip)-[previousPart:IS_PART_OF]->longTrip,
(place)-[:BELONGS_TO*]->(:Country{name:'Poland'})<-[BELONGS_TO]-(previousPlace)
WHERE previousPart.order_no = warsawPart.order_no -1
RETURN previousPlace.name as place
UNION
MATCH (warsawTrip:Trip)-[:TO]->(place:Place{name:'Warszawa'}),
(warsawTrip)-[warsawPart:IS_PART_OF]->(longTrip:Trip),
(nextPlace:Place)<-[:TO]-(nextTrip)-[nextPart:IS_PART_OF]->longTrip,
(place)-[:BELONGS_TO*]->(:Country{name:'Poland'})<-[BELONGS_TO]-(nextPlace)
WHERE nextPart.order_no = warsawPart.order_no +1
RETURN nextPlace.name as place

总结

展示的模型已经可以实现用例中所示的各种推荐功能,并且似乎还有进一步扩展的潜力。该模型可以通过添加额外的关系来丰富,例如 人可以 关注 另一个人,地点 与 另一个地点 接近,还可以向地点添加额外的标签,例如 岛屿,大陆。这些额外的关系和标签有助于提高旅行方向推荐的个性化程度。

© . All rights reserved.