Task: Refactor the Transport section
Refactor the Transport section
Currently, there are almost 1,500 legacy route records in the database. However, the data is stored in a flat structure like this:
{
"id": 4,
"city": "ho-chi-minh",
"city2": "mui-ne",
"time": "07:30",
"time2": "12:30",
"price": "120000",
"company": "Tam Hanh Cafe",
"type": "bus",
"gps_link": "https://goo.gl/maps/86122wbjUQ62",
"gps_link2": "https://goo.gl/maps/1Sabdd9TVy42",
"address": "199 Phạm Ngũ Lão, Quận 1, Hồ Chí Minh",
"address2": "135 Nguyễn Đình Chiểu, Phường Hàm Tiến, Thành phố Phan Thiết, Bình Thuận",
"phone": "+84 283 920 5653",
"note": ""
}
The issue here is that there is no separate reference table, at least for companies.
If we query the companies and transport types with grouping right now, we get the following picture:
| count(*) | company | type |
|---|---|---|
| 772 | Futa Bus | bus |
| 175 | Hanh Cafe | bus |
| 41 | Tam Hanh Cafe | bus |
| 79 | The Sinh Tourist | bus |
| 1 | LC3 | train |
| 1 | LC4 | train |
| 1 | MR1 | train |
| 1 | NH1 | train |
| 1 | NH2 | train |
| 34 | SE1 | train |
| 2 | SE1 (Livitrans Express) | train |
| 7 | SE1 (Violette Train) | train |
| 6 | SE17 | train |
| 8 | SE18 | train |
| 9 | SE19 | train |
| 29 | SE2 | train |
| 2 | SE2 (Livitrans Express) | train |
| 2 | SE2 (Violette Train) | train |
| 10 | SE20 | train |
| 2 | SE21 | train |
| 2 | SE22 | train |
| 1 | SE22 (Livitrans Express) | train |
| 35 | SE3 | train |
| 9 | SE3 (Violette Train) | train |
| 29 | SE4 | train |
| 2 | SE4 (Violette Train) | train |
| 34 | SE5 | train |
| 29 | SE6 | train |
| 34 | SE7 | train |
| 29 | SE8 | train |
| 1 | SNT1 | train |
| 1 | SNT2 | train |
| 1 | SNT2 (Livitrans Express) | train |
| 1 | SP1 | train |
| 1 | SP2 | train |
| 1 | SP3 | train |
| 1 | SP4 | train |
| 1 | SP7 | train |
| 1 | SP8 | train |
| 1 | SPT1 | train |
| 1 | SPT2 | train |
| 8 | Boat Company | boat |
| 4 | Greenlines DP | boat |
| 8 | Ka Long | boat |
| 14 | Phu Quoc Express | boat |
| 16 | Superdong | boat |
| 10 | The Sinh Tourist | boat |
Here, at least SP1, SP2, etc., are not companies at all, but train routes. Meanwhile, SNT2 (Livitrans Express) refers to specific carriage cars operated by Livitrans Express on train SNT2. So the current structure is a complete mess.
However, there is another problem: there are duplicate records.
| count(*) | company | city | city2 | time | address | address2 |
|---|---|---|---|---|---|---|
| 4 | Hanh Cafe | hue | ho-chi-minh | 08:30 | 28 Chu Văn An, Phú Hội, Thành phố Huế, Thừa Thiên ... | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... |
| 4 | Hanh Cafe | hue | ho-chi-minh | 13:30 | 28 Chu Văn An, Phú Hội, Thành phố Huế, Thừa Thiên ... | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... |
| 4 | Hanh Cafe | hoi-an | ho-chi-minh | 17:30 | 12 Nguyễn Tất Thành, Phường Cẩm Phổ, tp. Hội An, Q... | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... |
| 2 | Futa Bus | ho-chi-minh | rach-gia | 18:15 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 260A Nguyễn Bỉnh Khiêm, Vĩnh Thanh, Rạch Giá, tỉnh... |
| 2 | Hanh Cafe | nha-trang | ho-chi-minh | 07:30 | 10 Hùng Vương, Lộc Thọ, Thành phố Nha Trang, Khánh... | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... |
| 2 | Hanh Cafe | nha-trang | ho-chi-minh | 13:00 | 10 Hùng Vương, Lộc Thọ, Thành phố Nha Trang, Khánh... | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... |
| 2 | Futa Bus | ho-chi-minh | soc-trang | 22:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | ấp ngãi hội 2 , thị trấn đại ngãi , huyện long phú... |
| 2 | Hanh Cafe | phan-thiet | ha-noi | 01:30 | 173 Tôn Đức Thắng, Phú Thuỷ, Thành phố Phan Thiết,... | Nguyễn Khoái, Bạch Đằng, Hai Bà Trưng, Hà Nội |
| 2 | Hanh Cafe | ho-chi-minh | vinh-long | 07:45 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Lê Qúy Đôn, Khu 4, Thị Trấn Cái Bè, TT. Cái Bè, Cá... |
| 2 | Hanh Cafe | phan-thiet | ha-noi | 13:30 | 173 Tôn Đức Thắng, Phú Thuỷ, Thành phố Phan Thiết,... | Nguyễn Khoái, Bạch Đằng, Hai Bà Trưng, Hà Nội |
| 2 | Hanh Cafe | ho-chi-minh | vinh-long | 09:30 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Lê Qúy Đôn, Khu 4, Thị Trấn Cái Bè, TT. Cái Bè, Cá... |
| 2 | Futa Bus | ho-chi-minh | chau-doc | 07:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 69 Phan Văn Vàng, Châu Phú A, An Phú, An Giang, Vi... |
| 2 | Hanh Cafe | ho-chi-minh | vinh-long | 13:15 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Lê Qúy Đôn, Khu 4, Thị Trấn Cái Bè, TT. Cái Bè, Cá... |
| 2 | Futa Bus | ho-chi-minh | chau-doc | 08:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 69 Phan Văn Vàng, Châu Phú A, An Phú, An Giang, Vi... |
| 2 | Hanh Cafe | ho-chi-minh | vinh-long | 16:30 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Lê Qúy Đôn, Khu 4, Thị Trấn Cái Bè, TT. Cái Bè, Cá... |
| 2 | Hanh Cafe | ho-chi-minh | vinh-long | 19:15 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Lê Qúy Đôn, Khu 4, Thị Trấn Cái Bè, TT. Cái Bè, Cá... |
| 2 | Futa Bus | ho-chi-minh | sa-dec | 16:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 149/8 khóm Hòa Khánh, Phường 2, Sa Đéc, Đồng Tháp,... |
| 2 | Hanh Cafe | ho-chi-minh | vinh-long | 21:30 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Lê Qúy Đôn, Khu 4, Thị Trấn Cái Bè, TT. Cái Bè, Cá... |
| 2 | Futa Bus | ho-chi-minh | sa-dec | 17:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 149/8 khóm Hòa Khánh, Phường 2, Sa Đéc, Đồng Tháp,... |
| 2 | Futa Bus | ho-chi-minh | chau-doc | 12:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 69 Phan Văn Vàng, Châu Phú A, An Phú, An Giang, Vi... |
| 2 | Hanh Cafe | ho-chi-minh | can-tho | 07:45 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Mỹ Thuận, Bình Minh, Vinh Long |
| 2 | Futa Bus | ho-chi-minh | sa-dec | 18:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 149/8 khóm Hòa Khánh, Phường 2, Sa Đéc, Đồng Tháp,... |
| 2 | Hanh Cafe | ho-chi-minh | can-tho | 09:30 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Mỹ Thuận, Bình Minh, Vinh Long |
| 2 | Hanh Cafe | ho-chi-minh | can-tho | 13:15 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Mỹ Thuận, Bình Minh, Vinh Long |
| 2 | Hanh Cafe | ho-chi-minh | can-tho | 16:30 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Mỹ Thuận, Bình Minh, Vinh Long |
| 2 | Futa Bus | ho-chi-minh | chau-doc | 16:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 69 Phan Văn Vàng, Châu Phú A, An Phú, An Giang, Vi... |
| 2 | Hanh Cafe | ho-chi-minh | can-tho | 19:15 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Mỹ Thuận, Bình Minh, Vinh Long |
| 2 | Hanh Cafe | ho-chi-minh | can-tho | 21:30 | 229 Phạm Ngũ Lão, Phường Phạm Ngũ Lão, Quận 1, Hồ ... | Mỹ Thuận, Bình Minh, Vinh Long |
| 2 | Futa Bus | ho-chi-minh | chau-doc | 18:00 | 272 Đường Đề Thám, Phường Phạm Ngũ Lão, Quận 1, Hồ... | 69 Phan Văn Vàng, Châu Phú A, An Phú, An Giang, Vi... |
| 1 | Tam Hanh Cafe | mui-ne | ho-chi-minh | 05:00 | 135 Nguyễn Đình Chiểu, Phường Hàm Tiến, Thành phố ... | 199 Phạm Ngũ Lão, Quận 1, Hồ Chí Minh |
We need to get rid of duplicates already at the import stage. Fortunately, there weren't separate pages for each individual route. By the way, we'll need to create them.
Ворклоги
By the way, here is what ChatGPT says about our knowledge base structure:
The strongest part here is not the individual models, but how they fit together.
What is genuinely powerful here
KBConcept can essentially act as a universal site entity:
- country;
- region;
- city;
- district;
- beach;
- temple;
- airport;
- hotel;
- route;
- event;
- transport hub.
The concept already features:
Parent / Childrenhierarchy;- tree root;
lat / lng / altcoordinates;- multilingual names via
KBLabel; - files and images;
- facts;
- URL binding via
SiteRoute; - arbitrary structured data via
Json.
In other words, a city, a landmark, and a route are not just CMS articles, but nodes in a unified knowledge graph.
And this aligns perfectly with VietnamGuru.
For example:
Da Nang → Son Tra peninsula → Linh Ung Pagoda → coordinates → opening hours → pricing → seasonality → transport → nearby places → article → map → route.
All of this can be stored not as a single massive block of text, but as interconnected knowledge.
The most promising part — facts are not strictly tied to a single entity
KBFact together with KBFactParticipation supports n-ary relationships: a single fact can link multiple concepts at once with different roles.
For a travel project, this is an extremely powerful model.
For example, the statement:
Bus No. 5 goes from Da Nang to Hoi An, stops near the Marble Mountains, and the ticket costs 30,000 VND.
Can be represented as a single fact involving:
- Da Nang —
origin; - Hoi An —
destination; - Marble Mountains —
stop; - Bus No. 5 —
transport; - 30,000 VND — value;
- validity period;
- source;
- confidence;
- verification status.
And if the price or route changes, the old fact is not destroyed. A new one is added with a new validity date. For travel, this is critically important: visa rules, prices, schedules, entrance tickets, and transport are constantly changing.
A full-fledged epistemic model is built-in here
What I especially like is that the database does not pretend to hold absolute truth.
Facts have:
confidence;verified / tentative / disputed / deprecated;- source;
- validity period;
- the date the fact became known;
- raw or derived status;
- derivation from other facts.
Plus, contradictions are treated as independent objects via KBConflict rather than simply being overwritten.
For a travel AI, this is a massive advantage.
A typical real-world scenario:
- Google Maps says the place is open until 18:00;
- The official website says until 17:00;
- A recent review says the ticket counter closes at 16:30.
A standard RAG would just output one of these options. Your model allows the agent to say:
"Sources disagree. The official stated time is 17:00, but visitors report earlier ticket office closures. It's best to arrive before 16:00."
This is no longer just a chatbot sitting on top of articles, but a proper intelligent travel assistant.
KBKnowledgeSpace unlocks yet another layer
Knowledge Spaces allow the exact same fact to have different:
- visibility;
- importance;
- confidence level;
- interpretation.
This can be used not only for privacy, but also for different product modes:
- editorial base;
- public base;
- user personal space;
- specific trip space;
- agent knowledge;
- partner content;
- unverified user data.
For example, a tourist creates a trip "Vietnam, August, family with a child." The agent can build a separate knowledge space where higher priority is given to:
- short transits;
- family beaches;
- air-conditioned locations;
- safe food;
- places without difficult climbs.
The same facts exist globally, but their importance shifts in the context of the trip.
SiteRoute makes the site structure an extension of the knowledge graph
The SiteRoute ↔ KBConcept connection is a brilliant idea.
This means the URL is not the entity itself, but a representation of the entity.
As a result, you can:
- change routes without losing the concept;
- build breadcrumbs from the tree;
- generate pages automatically;
- create regional sections;
- support old URLs via redirects;
- find entities without a public page;
- give a single object different representations.
For SEO, this is exceptionally valuable. Content architecture can emerge naturally from the graph rather than being manually maintained across multiple disconnected tables.
The map can actually become the interface to the knowledge base here
Now your explanation about the map looks even more interesting.
Since both KBConcept and File have coordinates, the map can display not just points from articles, but an entire spatial model:
- cities;
- landmarks;
- place-tagged photos;
- routes;
- nearby places;
- facts with geographical context;
- user saves;
- zones of interest.
And the agent can handle queries like:
"Show me interesting places within a 15 km radius of Nha Trang that are accessible without a motorbike and worth visiting after 17:00."
A standard article is not enough for such a query. But this schema potentially allows assembling an answer from coordinates, facts, relations, constraints, and user preferences.
By the way, some routes also have corrupted data.
"Can not get city2: \"cat-ba-island\"",
"Can not get city2: \"cat-ba-island\"",
"Can not get city2: \"cat-ba-island\"",
"Can not get city: \"cat-ba-island\"",
"Can not get city: \"cat-ba-island\"",
"Can not get city: \"cat-ba-island\"",
"Can not get city2: \"phong-nha-ke-bang\"",
"Can not get city2: \"nam-du-island\"",
"Can not get city2: \"nam-du-island\"",
"Can not get city2: \"nam-du-island\"",
"Can not get city: \"nam-du-island\"",
"Can not get city: \"nam-du-island\"",
Need to fix this along the way.