GraphGists

想法

这项工作的目标是展示使用从公共机构来源获取的开放数据来构建一个有趣的服务是可能的(至少从我的角度来看:) )。这个示例服务简化了旅行,并使用了来自联合国欧洲经济委员会 (UNECE) 的关于国家和城市的公共数据,但也通过从OpenStreetMap 获取的餐厅和旅馆信息进行了丰富。目前,即使你不富有,你也可以负担得起环游世界——有廉价航空公司,你可以免费住在别人的沙发上,在互联网上找到大量关于如何环游世界而不被毁坏的有用建议。如今,问题实际上可能是**去哪里**度周末,短途旅行或一次毕生难忘的冒险,因为选择太多了。旅行助手服务旨在为人们推荐旅行路线和设施。

数据

为了实现上面介绍的服务的基本功能,需要以下数据

  • 关于不同地点的数据:国家、地区、城市 - 在这种情况下,开放数据是从联合国欧洲经济委员会 (UNECE) 获取的。

  • 关于POI的数据:酒店和餐厅,可以从OpenStreetMap 获取。这些数据根据开放数据库许可证 提供,并且是**© OpenStreetMap 贡献者**的财产

  • 所有这些都应该用关于人们、他们去过的地方以及去过餐厅和酒吧的信息进行丰富。我相信这些缺失的数据可以由Travel Help服务本身收集。

数据模型

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

fSZBomW

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

表 1. 域
属性 示例

姓名、年龄、博客地址

Bill,32岁

地点:国家

名称、代码

波兰

地点:地区

名称、代码、类型

普罗旺斯 (法国)

地点:城市

名称、代码、坐标

华沙

住宿场所

名称、网站、地址

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

食物

名称、网站、地址、菜系

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

旅行

类型、持续时间、年份季节

环游欧洲旅行,伦敦周末游

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

地点

属于

地点

华沙属于波兰

居住 (从,到)

地点

Kate从2014年起居住在莫斯科

为…而行

旅行

Bob为环游世界而旅行

旅行

前往 (交通工具)

地点

前往伦敦旅行 (交通工具 = 飞机)

旅行

从…开始

地点

前往伦敦的旅行从柏林开始

旅行

是…的一部分 (顺序号)

旅行

前往伦敦的旅行是环游世界的旅行的一部分 (顺序号 = 1)

旅行

入住 (评分、每晚平均价格)

住宿场所

在前往伦敦的旅行期间,Kate入住希尔顿酒店 (评分 = 5,每晚平均价格 = 1000)

旅行

去过 (评分、平均消费)

食物

在前往伦敦的旅行期间,Kate去过Dawsan餐厅 (评分 = 5,平均消费 = 1000)

食物

位于

地点

Dawsan餐厅位于伦敦

住宿场所

位于

地点

希尔顿酒店位于伦敦

图数据上传

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

上传的数据包含关于人们、地点以及这些人去过不同地方的信息

地点

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

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

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

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

  • **葡萄牙**:波尔图、里斯本、卡斯卡伊斯、法鲁

  • **英国**:伦敦、格拉斯哥、曼彻斯特、卡迪夫

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

关于这些地点的数据来自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 获取的,并被翻译成CSV文件。

//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岁,来自西班牙马德里,在伦敦度过了一个周末

  • 约翰,34岁,来自西班牙马德里,在巴塞罗那度过了一个周末。

  • 克劳迪娅,26岁,来自葡萄牙里斯本,在波兰各地旅行。

  • 诺拉,18岁,来自美国芝加哥,在波兰各地旅行。

  • 卢卡斯,30岁,来自波兰华沙,在欧洲各地旅行。

  • 佩德罗,32岁,来自意大利罗马,在波兰各地旅行。

  • 皮埃尔,40岁,来自法国尼斯,在波兰各地旅行。

  • 劳拉,31岁,来自西班牙马德里,正在寻找旅行灵感。

用例

收集了有关人们及其前往不同地点的旅行数据后,就可以使用这些数据来推荐可能更适合人们需求的地点和设施。示例用例可以分为两组:在提前计划假期时使用 TravelHelper 以及在假期期间需要帮助时使用 TravelHelper。以下示例假设寻求帮助的人是劳拉,31岁,来自西班牙马德里。

1. 如何使用 TravelHelper 计划假期?

1a. 我是劳拉,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. 我是劳拉,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. 我是劳拉,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. 如何在假期期间临时使用 TravelHelper?

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. 我是劳拉,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

总结

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