From 3648366ebe9b09e9dec336d42951b9c49c6471fc Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:14:18 +0700 Subject: [PATCH] feat(geo): graph-based hubs for product --- geo_app/schema.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/geo_app/schema.py b/geo_app/schema.py index bb77261..760eb09 100644 --- a/geo_app/schema.py +++ b/geo_app/schema.py @@ -334,6 +334,7 @@ class Query(graphene.ObjectType): 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"), + use_graph=graphene.Boolean(default_value=False, description="Use graph-based reachability for product filter"), limit=graphene.Int(default_value=12, description="Max results"), description="Find nearest hubs to coordinates (optionally filtered by product)", ) @@ -1166,6 +1167,58 @@ class Query(graphene.ObjectType): logger.error("Error getting hubs for product: %s", e) return [] + def resolve_hubs_for_product_graph(self, info, product_uuid, limit=500): + """Get hubs that can be reached by graph routes for a product.""" + db = get_db() + ensure_graph() + + aql = """ + FOR hub IN nodes + FILTER hub.node_type == 'logistics' OR hub.node_type == 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 + RETURN hub + """ + try: + cursor = db.aql.execute(aql) + hubs_with_distance = [] + for hub in cursor: + hub_uuid = hub.get('_key') + if not hub_uuid: + continue + try: + routes = self.resolve_offers_by_hub(info, hub_uuid, product_uuid, limit=1) + except Exception as route_error: + logger.error("Error resolving offers for hub %s: %s", hub_uuid, route_error) + continue + if not routes: + continue + distance_km = routes[0].distance_km if routes[0] else None + hubs_with_distance.append((distance_km, hub)) + + # Sort by graph distance when available + hubs_with_distance.sort(key=lambda item: (item[0] is None, item[0] or 0)) + + hubs = [] + for distance_km, hub in hubs_with_distance[:limit]: + hubs.append(NodeType( + uuid=hub.get('_key'), + name=hub.get('name'), + latitude=hub.get('latitude'), + longitude=hub.get('longitude'), + country=hub.get('country'), + country_code=hub.get('country_code'), + transport_types=hub.get('transport_types'), + distance_km=distance_km, + )) + + logger.info("Found %d graph-reachable hubs for product %s", len(hubs), product_uuid) + return hubs + except Exception as e: + logger.error("Error getting graph hubs for product %s: %s", product_uuid, e) + return [] + def resolve_offers_by_hub(self, info, hub_uuid, product_uuid, limit=10): """ Get offers for a product with routes to hub. @@ -1510,7 +1563,7 @@ 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, source_uuid=None, limit=12): + def resolve_nearest_hubs(self, info, lat, lon, radius=1000, product_uuid=None, source_uuid=None, use_graph=False, limit=12): """Find nearest hubs to coordinates, optionally filtered by product availability.""" db = get_db() @@ -1601,6 +1654,9 @@ class Query(graphene.ObjectType): )) return hubs + if product_uuid and use_graph: + return self.resolve_hubs_for_product_graph(info, product_uuid, limit=limit) + if product_uuid: # Find hubs that have offers for this product within radius aql = """