feat: turn search_trips into the website's travel search

This commit is contained in:
fromme committed 2026-10-08 13:14:12 +02:00
1 parent bfdedefae3
commit 4ab2714bde
12 files changed
+693 -151

No files matched your search

+30 -5
View File
@@ -39,7 +39,7 @@ that prices and availability are a snapshot (`dataAsOf`) and only the booking pa
| Tool | Use | Returns |
|---|---|---|
| `search_trips` | Which trips exist, when, where, how much | Up to 10 trips with their next 3 matching dates, `priceFrom`, availability, `dataAsOf` |
| `search_trips` | The website's travel search: which trips exist, when, where, how much | One result per trip × hotel × travel date (default 10, max 25), with "ab" price, nights, inclusions, room types, availability, link; `total` and `dataAsOf` |
| `get_trip` | One trip by product `code`, or one date of it by date code such as `SSLMP061226` (optionally `hotelCode`) | Product text with FAQs, hotels with facts, all upcoming dates with bookable rooms, pickups for the first 3 dates |
| `search_content` | Questions not about dates or prices | Up to 8 hits with a ~600-character excerpt |
| `get_document` | The full text of one `search_content` hit, by `url` | Markdown body, truncated at 20 000 characters |
@@ -52,15 +52,40 @@ Trip rules (`App\Chatbot\TripCatalog`):
- TYPO3 products are joined to the export on `product.code`. Several products can share a code, and
each is listed as its own trip. Hotels are joined on any entry of the TYPO3 hotel's `codes` (BusPro
uses the matchcode aliases). A code that is one hotel's own and another's alias resolves to its owner.
- A room is bookable unless its status is `Buchungsstop`. `available` counts rooms, so the places of a
date at a hotel are `Σ available × maxPax` over its bookable rooms. `persons` filters on that sum, and
`maxPrice` on the lowest bookable room price. The tools never output room counts, only `frei` or
`auf Anfrage`.
- A room is bookable unless its status is `Buchungsstop`. The baby row (room code `Baby`) is not a room.
`available` counts rooms, so the places of a date at a hotel are `Σ available × maxPax` over its
bookable rooms. The tools never output room counts, only `frei` or `auf Anfrage`.
- `search_trips` only offers products that TYPO3 lists in its own website search (`searchable`) and does
not mark as unbookable. Closed group trips and staff trainings are `searchable: false`. `get_trip` still
answers when someone names their code.
- The export's `guide` (name, phone) and all BusPro ids are never output.
`search_trips` mirrors the search page (`/skireisen/suche/`), whose data is TYPO3's import of our own
product export. Its rules follow TYPO3's `DateImportService` and search repository, derived per date and
hotel in `App\Model\Chatbot\TravelDateFacts`:
- **"ab" price:** the cheapest bookable room with places left, otherwise the cheapest bookable room.
`priceMin`/`priceMax` apply to it. The site's price bands are presets of this.
- **Travel period:** `dateFrom`/`dateTo` match by containment (start on or after, end on or before), as on
the site, not by overlap. `period` names one of the site's presets, from the `chatbot_travel_periods`
parameter in `config/services.yaml`. It's duplicated from the TYPO3 search configuration on purpose, so
update both each season. Expired periods are no longer offered.
- **`persons`:** the site's condition (a room takes groups this small: smallest `minPax` ≤ persons) **and**
enough places left. The site checks only the first.
- **Room type:** capacity, as TYPO3's `RoomTypeResolver` (`App\Enum\RoomType`): `maxPax` 1–8, 9 or more
is `9er+`. A label containing "bett im" or "mit ander" is `mit anderen` (a shared bed, sold per bed).
- **Accommodation:** the TYPO3 hotel `type` (1 Ferienwohnung, 2 Sportclub, 3 Apartment, 4 Gästehaus,
5 Pension), `App\Enum\HotelType`.
- **Board:** the items of selection group 71 ("4x Abendessen", "Frühstück inkl. … Lunchpaket", "Vollpension",
…), matched by word against any item. The site keys on the first item only. "Halbpension" occurs in no
label, so dinner is asked as "Abendessen".
- **Inclusions:** selection groups 7 (Skipass), 71 (board), 86 (Programm), 87 (Kurse), 89 (Events), and the
outbound bus when its price is 0. `bus: true` asks for any bookable outbound bus.
- **Nights:** days between start and end. The site's "short" is ≤ 4 and its "long" is ≥ 5.
- **Order:** by start date, then "ab" price. The site sorts by concept first, which the feed doesn't carry.
- Results link to the product page. Links with the hotel preselected and the site's "Restplätze" badge need
two fields the TYPO3 content feed doesn't send yet (`hotelUrls`, `lowContingent`).
Search (`App\Chatbot\Search\ContentSearch`) is MariaDB full text in boolean mode over
`title, breadcrumb, body_md`. Each term becomes a prefix match, because InnoDB has no German stemming,
and German stopwords are dropped in PHP. Relevance is weighted by type: products and hotels, then