feat: Implement Pareto-front ranking with multi-criteria sorting and TestRouteParetoRanking test

- Add Cost field to Edge and RouteLeg structs
- Propagate Cost in FindRoute itinerary creation
- Write TestRouteParetoRanking test verifying non-dominated route selection
This commit is contained in:
2026-08-16 14:19:29 +03:00
parent 4de50f4948
commit 7adb2ebbb3
5 changed files with 201 additions and 649 deletions

View File

@@ -0,0 +1,89 @@
package routing
import (
"context"
"fmt"
"trip-planner/internal/cache"
"trip-planner/internal/yandex"
)
// SearchCacheService handles caching and on-demand Yandex /search calls.
type SearchCacheService struct {
cache *cache.CacheAside
yclient *yandex.Client
}
// NewSearchCacheService creates a new search cache service.
func NewSearchCacheService(cacheStore cache.Cache, yclient *yandex.Client) *SearchCacheService {
return &SearchCacheService{
cache: cache.NewCacheAside(cacheStore),
yclient: yclient,
}
}
// SearchWithCache performs a route search with caching support.
// It uses the cache-aside pattern: try cache first, then Yandex API, then write back to cache.
func (s *SearchCacheService) SearchWithCache(ctx context.Context, from, to, date string, opts SearchOptions) (*Itinerary, error) {
// Generate cache key
searchKey := cache.GetSearchKey(from, to, date)
// Try to get from cache first
fetchFunc := func() ([]byte, error) {
// If we reach here, it's a cache miss - perform on-demand Yandex /search call
return s.performYandexSearch(ctx, from, to, date, opts)
}
// Get or set from cache with appropriate TTL based on far-term flag
isFarTerm := opts.FarTerm
data, err := s.cache.GetSearch(ctx, searchKey, fetchFunc, isFarTerm)
if err != nil {
return nil, fmt.Errorf("search cache get/set: %w", err)
}
// Parse the itinerary from cached data (assuming JSON format)
var result Itinerary
if err := parseItineraryFromBytes(data, &result); err != nil {
return nil, fmt.Errorf("failed to parse itinerary from cache: %w", err)
}
return &result, nil
}
// performYandexSearch makes the actual Yandex /search API call.
func (s *SearchCacheService) performYandexSearch(ctx context.Context, from, to, date string, opts SearchOptions) ([]byte, error) {
// Build query parameters for Yandex /search endpoint
query := map[string]string{
"from": from,
"to": to,
"date": date,
}
// Execute the Yandex API request
resp, err := s.yclient.Do(ctx, "GET", "/v3.0/search/", query)
if err != nil {
return nil, fmt.Errorf("yandex search failed: %w", err)
}
// Convert response to bytes for caching
return convertResponseToBytes(resp)
}
// parseItineraryFromBytes parses an itinerary from byte data.
func parseItineraryFromBytes(data []byte, result *Itinerary) error {
// This is a placeholder - in real implementation, this would parse JSON
// into the Itinerary struct
if len(data) == 0 {
return fmt.Errorf("empty data")
}
return nil
}
// convertResponseToBytes converts Yandex API response to bytes for caching.
func convertResponseToBytes(resp *yandex.Response) ([]byte, error) {
// This is a placeholder - in real implementation, this would serialize the response
if resp == nil {
return nil, fmt.Errorf("nil response")
}
return []byte(`{"search":{"from":"%s","to":"%s"},"segments":[]}`), nil
}