Filter hubs to rail/sea and add graph-based nearest
All checks were successful
Build Docker Image / build (push) Successful in 3m9s

This commit is contained in:
Ruslan Bakiev
2026-02-05 18:41:07 +07:00
parent 387abf03e4
commit 09324bb25e
2 changed files with 108 additions and 7 deletions

View File

@@ -81,11 +81,18 @@ def _fetch_nodes(db, west, south, east, north, transport_type=None, node_type=No
nodes = list(cursor)
# Filter by transport type if specified (only for logistics nodes)
if transport_type and node_type in (None, 'logistics'):
nodes = [
n for n in nodes
if transport_type in (n.get('transport_types') or [])
]
if node_type in (None, 'logistics'):
if transport_type:
nodes = [
n for n in nodes
if transport_type in (n.get('transport_types') or [])
]
else:
# Default: only rail/sea hubs
nodes = [
n for n in nodes
if ('rail' in (n.get('transport_types') or [])) or ('sea' in (n.get('transport_types') or []))
]
return nodes
@@ -151,4 +158,3 @@ def get_clustered_nodes(db, west, south, east, north, zoom, transport_type=None,
logger.info("Returning %d clusters/points for zoom=%d res=%d", len(results), zoom, resolution)
return results

View File

@@ -325,6 +325,7 @@ class Query(graphene.ObjectType):
lon=graphene.Float(required=True, description="Longitude"),
radius=graphene.Float(default_value=1000, description="Search radius in km"),
product_uuid=graphene.String(description="Filter hubs by product availability"),
source_uuid=graphene.String(description="Source node UUID for graph-based nearest hubs"),
limit=graphene.Int(default_value=12, description="Max results"),
description="Find nearest hubs to coordinates (optionally filtered by product)",
)
@@ -622,6 +623,8 @@ class Query(graphene.ObjectType):
aql = """
FOR node IN nodes
FILTER node.node_type == 'logistics' OR node.node_type == null
LET types = node.transport_types != null ? node.transport_types : []
FILTER ('rail' IN types) OR ('sea' IN types)
FILTER node.country != null
COLLECT country = node.country
SORT country ASC
@@ -1462,10 +1465,97 @@ class Query(graphene.ObjectType):
logger.info("No route found from offer %s to hub %s", offer_uuid, hub_uuid)
return None
def resolve_nearest_hubs(self, info, lat, lon, radius=1000, product_uuid=None, limit=12):
def resolve_nearest_hubs(self, info, lat, lon, radius=1000, product_uuid=None, source_uuid=None, limit=12):
"""Find nearest hubs to coordinates, optionally filtered by product availability."""
db = get_db()
# Graph-based nearest hubs when source_uuid provided
if source_uuid:
nodes_col = db.collection('nodes')
start = nodes_col.get(source_uuid)
if not start:
logger.warning("Source node %s not found for nearest hubs", source_uuid)
return []
def is_target_hub(doc):
if doc.get('_key') == source_uuid:
return False
if doc.get('node_type') not in ('logistics', None):
return False
types = doc.get('transport_types') or []
return ('rail' in types) or ('sea' in types)
def fetch_neighbors(node_key):
aql = """
FOR edge IN edges
FILTER edge.transport_type IN ['auto', 'rail', 'offer']
FILTER edge._from == @node_id OR edge._to == @node_id
LET neighbor_id = edge._from == @node_id ? edge._to : edge._from
LET neighbor = DOCUMENT(neighbor_id)
FILTER neighbor != null
RETURN {
neighbor_key: neighbor._key,
neighbor_doc: neighbor,
transport_type: edge.transport_type,
distance_km: edge.distance_km,
travel_time_seconds: edge.travel_time_seconds
}
"""
cursor = db.aql.execute(
aql,
bind_vars={'node_id': f"nodes/{node_key}"},
)
return list(cursor)
queue = []
counter = 0
heapq.heappush(queue, (0, counter, source_uuid))
visited = {}
node_docs = {source_uuid: start}
found = []
expansions = 0
while queue and len(found) < limit and expansions < Query.MAX_EXPANSIONS:
cost, _, node_key = heapq.heappop(queue)
if node_key in visited and cost > visited[node_key]:
continue
visited[node_key] = cost
node_doc = node_docs.get(node_key)
if node_doc and is_target_hub(node_doc):
found.append(node_doc)
if len(found) >= limit:
break
neighbors = fetch_neighbors(node_key)
expansions += 1
for neighbor in neighbors:
neighbor_key = neighbor.get('neighbor_key')
if not neighbor_key:
continue
node_docs[neighbor_key] = neighbor.get('neighbor_doc')
step_cost = neighbor.get('travel_time_seconds') or neighbor.get('distance_km') or 0
new_cost = cost + step_cost
if neighbor_key in visited and new_cost >= visited[neighbor_key]:
continue
counter += 1
heapq.heappush(queue, (new_cost, counter, neighbor_key))
hubs = []
for node in found:
hubs.append(NodeType(
uuid=node.get('_key'),
name=node.get('name'),
latitude=node.get('latitude'),
longitude=node.get('longitude'),
country=node.get('country'),
country_code=node.get('country_code'),
synced_at=node.get('synced_at'),
transport_types=node.get('transport_types') or [],
edges=[],
))
return hubs
if product_uuid:
# Find hubs that have offers for this product within radius
aql = """
@@ -1479,6 +1569,8 @@ class Query(graphene.ObjectType):
FOR hub IN nodes
FILTER hub.node_type == 'logistics' OR hub.node_type == null
FILTER hub.product_uuid == null
LET types = hub.transport_types != null ? hub.transport_types : []
FILTER ('rail' IN types) OR ('sea' IN types)
FILTER hub.latitude != null AND hub.longitude != null
LET dist_to_hub = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
FILTER dist_to_hub <= @radius
@@ -1496,6 +1588,8 @@ class Query(graphene.ObjectType):
FOR hub IN nodes
FILTER hub.node_type == 'logistics' OR hub.node_type == null
FILTER hub.product_uuid == null
LET types = hub.transport_types != null ? hub.transport_types : []
FILTER ('rail' IN types) OR ('sea' IN types)
FILTER hub.latitude != null AND hub.longitude != null
LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
FILTER dist <= @radius
@@ -1715,6 +1809,7 @@ class Query(graphene.ObjectType):
FILTER node.product_uuid == null
LET types = node.transport_types != null ? node.transport_types : []
FILTER @transport_type == null OR @transport_type IN types
FILTER @transport_type != null OR ('rail' IN types) OR ('sea' IN types)
FILTER @country == null OR node.country == @country
{bounds_filter}
SORT node.name ASC