feat: Implement Pareto-front ranking integration with multi-criteria sorting

- Add RankingMode field to SearchOptions (fastest/fewest_transfers/cheapest)
- Update FindRoutesPareto to respect ranking mode when sorting
- Add ranking_mode query parameter to RouteSearch endpoint
- Add TestParetoFrontGeneration with subtests for all three modes

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-16 19:27:02 +03:00
parent 026b779cb3
commit d67bc5aca7
4 changed files with 226 additions and 20 deletions

View File

@@ -764,6 +764,10 @@ type SearchOptions struct {
MCT int
// FarTerm indicates if the search date is far-term (affects caching/TTL).
FarTerm bool
// RankingMode determines the ranking/sort order for Pareto-optimal routes.
// Supported values: "fastest" (default, sort by duration), "fewest_transfers" (sort by number of transfers),
// "cheapest" (sort by cost).
RankingMode string
}
// Itinerary represents a complete route with legs and summary metrics.
@@ -798,7 +802,9 @@ type SearchResult struct {
// FindRoutesPareto finds Pareto-optimal routes (time, transfers, cost) from origin to destination.
// It runs the search algorithm and returns multiple routes that are not dominated by any other
// route in all three metrics simultaneously.
// route in all three metrics simultaneously. Routes are sorted according to the RankingMode
// in SearchOptions: "fastest" (default, by duration), "fewest_transfers" (by transfers),
// or "cheapest" (by cost).
func (g *Graph) FindRoutesPareto(originID, destID string, opts SearchOptions) []*Itinerary {
// Run multiple searches with different strategies to find diverse routes
var allItineraries []*Itinerary
@@ -814,16 +820,39 @@ func (g *Graph) FindRoutesPareto(originID, destID string, opts SearchOptions) []
}
}
// Sort by total duration (primary), then transfers (secondary), then cost (tertiary)
sort.Slice(allItineraries, func(i, j int) bool {
if allItineraries[i].TotalDuration != allItineraries[j].TotalDuration {
return allItineraries[i].TotalDuration < allItineraries[j].TotalDuration
}
if allItineraries[i].TotalTransfers != allItineraries[j].TotalTransfers {
// Sort according to the specified RankingMode
switch opts.RankingMode {
case "fewest_transfers":
sort.Slice(allItineraries, func(i, j int) bool {
if allItineraries[i].TotalTransfers != allItineraries[j].TotalTransfers {
return allItineraries[i].TotalTransfers < allItineraries[j].TotalTransfers
}
if allItineraries[i].TotalDuration != allItineraries[j].TotalDuration {
return allItineraries[i].TotalDuration < allItineraries[j].TotalDuration
}
return allItineraries[i].Cost < allItineraries[j].Cost
})
case "cheapest":
sort.Slice(allItineraries, func(i, j int) bool {
if allItineraries[i].Cost != allItineraries[j].Cost {
return allItineraries[i].Cost < allItineraries[j].Cost
}
if allItineraries[i].TotalDuration != allItineraries[j].TotalDuration {
return allItineraries[i].TotalDuration < allItineraries[j].TotalDuration
}
return allItineraries[i].TotalTransfers < allItineraries[j].TotalTransfers
}
return allItineraries[i].Cost < allItineraries[j].Cost
})
})
default: // "fastest" or any other value - sort by duration (primary), transfers (secondary), cost (tertiary)
sort.Slice(allItineraries, func(i, j int) bool {
if allItineraries[i].TotalDuration != allItineraries[j].TotalDuration {
return allItineraries[i].TotalDuration < allItineraries[j].TotalDuration
}
if allItineraries[i].TotalTransfers != allItineraries[j].TotalTransfers {
return allItineraries[i].TotalTransfers < allItineraries[j].TotalTransfers
}
return allItineraries[i].Cost < allItineraries[j].Cost
})
}
// Pareto filter: remove dominated routes
// A route is dominated if another route is better or equal in all metrics (time, transfers, cost)