feat: implement lazy graph expansion FindRoute with on-demand /search and transfer depth limit

This commit is contained in:
2026-08-14 15:27:17 +03:00
parent bad78ea2fa
commit 0f95e3e2f8
3 changed files with 237 additions and 10 deletions

View File

@@ -585,10 +585,11 @@ func (g *Graph) NodesByID(id string) *Node {
}
// FindRoute performs BFS/Dijkstra search from origin to destination with a transfer depth limit.
// It returns the best itinerary found within the transfer limit.
// It uses lazy graph expansion to add edges on-demand during BFS, staying within API quota constraints.
// Returns the best itinerary found within the transfer limit.
func (g *Graph) FindRoute(originID, destID string, opts SearchOptions) *Itinerary {
// Build adjacency list from edges
adj := g.buildAdjacencyList()
// Track which nodes have been lazily expanded to avoid re-expansion
expanded := make(map[string]bool)
// BFS with transfer tracking
// State: (nodeID, transfersUsed, accumulatedDuration, lastArrivalTime, path)
@@ -645,10 +646,27 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions) *Itinerar
}
// Prune if we've exceeded max transfers
if opts.MaxTransfers >= 0 && current.transfers >= opts.MaxTransfers {
// Use > instead of >= to allow exploring from states at the exact transfer limit
if opts.MaxTransfers >= 0 && current.transfers > opts.MaxTransfers {
continue
}
// Lazily expand this node's adjacency list if not already expanded
if !expanded[current.nodeID] {
// Expand from this node using lazy expansion
// Use the destination city code and date from search options for /search calls
if opts.DestCityCode != "" && opts.Date != "" {
g.ExpandGraphLazy(g.currentNodeByID(current.nodeID), opts.DestCityCode, opts.Date)
} else {
// If no dest city/code available, add synthetic edges as fallback
addSyntheticEdgesForNode(g, current.nodeID)
}
expanded[current.nodeID] = true
}
// Get edges for this node - include both pre-built and lazily added edges
adj := g.buildAdjacencyList()
// Explore outgoing edges
for _, edge := range adj[current.nodeID] {
nextNode := edge.To
@@ -733,6 +751,77 @@ func (g *Graph) FindRoute(originID, destID string, opts SearchOptions) *Itinerar
return best
}
// currentNodeByID returns a node by its ID from the graph's nodes.
func (g *Graph) currentNodeByID(id string) *Node {
for _, n := range g.nodes {
if n.ID == id {
return n
}
}
return nil
}
// addSyntheticEdgesForNode adds synthetic transfer edges for a node as fallback
// when lazy expansion cannot call /search (e.g., no Yandex client or missing dest city code).
func addSyntheticEdgesForNode(g *Graph, nodeID string) {
// Find the node and add synthetic edges connecting it to its city hub
node := g.currentNodeByID(nodeID)
if node == nil {
return
}
// Determine the city code from the node
cityCode := node.CityCode
if cityCode == "" {
return
}
cityNodeID := "city:" + cityCode
destCityNode := g.NodesByID(cityNodeID)
if destCityNode == nil {
destCityNode = &Node{
ID: cityNodeID,
Type: NodeTypeCity,
Name: cityCode,
}
g.AddNode(destCityNode)
}
// Add synthetic edges: node <-> city hub
alreadyForward := false
alreadyReverse := false
for _, e := range g.edges {
if e.From != nil && e.From.ID == node.ID && e.To != nil && e.To.ID == destCityNode.ID {
alreadyForward = true
}
if e.From != nil && e.From.ID == destCityNode.ID && e.To != nil && e.To.ID == node.ID {
alreadyReverse = true
}
}
if !alreadyForward {
g.AddEdge(&Edge{
From: node,
To: destCityNode,
Kind: EdgeKindSynthetic,
Duration: 300, // 5 min synthetic transfer
Transport: "train",
IsTransfer: true,
})
}
if !alreadyReverse {
g.AddEdge(&Edge{
From: destCityNode,
To: node,
Kind: EdgeKindSynthetic,
Duration: 300, // 5 min synthetic transfer
Transport: "train",
IsTransfer: true,
})
}
}
// ApplyMCT applies Minimum Connection Time rules to the itinerary.
// It adjusts transfer times based on node types, city tiers, and check-in requirements.
func (g *Graph) ApplyMCT(itinerary *Itinerary, mctBase int) *Itinerary {
@@ -786,6 +875,10 @@ type SearchOptions struct {
MCT int
// FarTerm indicates if the search date is far-term (affects caching/TTL).
FarTerm bool
// DestCityCode is the destination city code for lazy graph expansion.
DestCityCode string
// Date is the search date for lazy graph expansion TTL policies.
Date string
}
// Itinerary represents a complete route with legs and summary metrics.