旅行助手
想法
这项工作的目的是展示,利用从公共机构获取的开放数据,可以构建一个有趣的服务(至少在我看来是这样:))。这个示例服务方便了旅行,并使用了由联合国欧洲经济委员会 (UNECE) 提供的关于国家和城市的公共数据,同时还补充了从OpenStreetMap 获取的关于餐馆和旅馆的信息。如今,即使你不是很富有,也可以负担得起环游世界——有廉价航空公司,你可以免费睡在别人家的沙发上,在网上找到大量关于如何看世界而不至于破产的有用技巧。现如今,问题可能在于去哪里度过一个周末、一次短途旅行或一次一生难忘的冒险,因为选择的可能性非常巨大。旅行助手服务旨在为人们推荐旅行方向和设施。
数据
为了实现上述服务的基本功能,需要以下数据:
-
有关不同地点的数据:国家、地区、城市——在本例中,开放数据从联合国欧洲经济委员会 (UNECE) 获取。
-
POI 数据:可以从OpenStreetMap 获取的酒店和餐馆。这些数据在开放数据库许可下可用,并且是© OpenStreetMap 贡献者的财产。
-
所有这些都应该补充有关人员、他们到各地旅行以及参观餐馆、酒吧的信息。我相信这些缺失的数据可以由旅行助手服务自己收集。
数据模型
设计的数据模型如下图所示:

模型中的节点和关系以及相应的示例显示在下表中:
域 | 属性 | 示例 |
---|---|---|
人 |
姓名, 年龄, 博客地址 |
Bill, 32 岁 |
地点:国家 |
名称, 代码 |
波兰 |
地点:地区 |
名称, 代码, 类型 |
普罗旺斯 (法国) |
地点:城市 |
名称, 代码, 坐标 |
华沙 |
住宿地 |
名称, 网站, 地址 |
纽约希尔顿酒店, 苏格兰高地营地 |
餐饮场所 |
名称, 网站, 地址, 菜系 |
巴黎薄饼店, 芝加哥汉堡店 |
旅行 |
类型, 时长, 年份季节 |
欧洲之旅, 伦敦周末游 |
起始节点 | 关系 | 结束节点 | 示例 |
---|---|---|---|
地点 |
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 岁,来自西班牙马德里。
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
2a. 我是 Laura,31 岁,来自马德里。目前正在波兰华沙旅游。华沙有哪些餐馆最受我这个年龄段的人推荐?
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
2b. 我是 Laura,31 岁,来自马德里。目前正在波兰华沙旅游。我不喜欢我的酒店。请向我展示其他人在期望价格范围内推荐的酒店。
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
总结
展示的模型已经可以实现用例中所示的各种推荐功能,并且似乎还有进一步扩展的潜力。该模型可以通过添加额外的关系来丰富,例如 人可以 关注 另一个人,地点 与 另一个地点 接近,还可以向地点添加额外的标签,例如 岛屿,大陆。这些额外的关系和标签有助于提高旅行方向推荐的个性化程度。
此页面有帮助吗?