Replace graph traversal queries with DISTANCE() queries
All checks were successful
Build Docker Image / build (push) Successful in 1m53s

- Add new resolvers: products, offersByProduct, hubsNearOffer, suppliers,
  productsBySupplier, offersBySupplierProduct, productsNearHub, offersToHub, deliveryToHub
- Remove broken queries that caused OOM on 234k edges
- Use DISTANCE() for geographic proximity instead of graph traversal
This commit is contained in:
Ruslan Bakiev
2026-01-16 15:39:55 +07:00
parent a3b0b5ff79
commit b6f9b2d70b

View File

@@ -91,6 +91,33 @@ class ClusterPointType(graphene.ObjectType):
name = graphene.String(description="Node name (only for single points)") name = graphene.String(description="Node name (only for single points)")
class ProductType(graphene.ObjectType):
"""Unique product from offers."""
uuid = graphene.String()
name = graphene.String()
class SupplierType(graphene.ObjectType):
"""Unique supplier from offers."""
uuid = graphene.String()
class OfferNodeType(graphene.ObjectType):
"""Offer node with location and product info."""
uuid = graphene.String()
product_uuid = graphene.String()
product_name = graphene.String()
supplier_uuid = graphene.String()
latitude = graphene.Float()
longitude = graphene.Float()
country = graphene.String()
country_code = graphene.String()
price_per_unit = graphene.String()
currency = graphene.String()
quantity = graphene.String()
unit = graphene.String()
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
"""Root query.""" """Root query."""
MAX_EXPANSIONS = 20000 MAX_EXPANSIONS = 20000
@@ -183,32 +210,63 @@ class Query(graphene.ObjectType):
description="Get clustered nodes for map display (server-side clustering)", description="Get clustered nodes for map display (server-side clustering)",
) )
# Business-oriented queries for catalog navigation # Catalog navigation queries
find_products_for_hub = graphene.List( products = graphene.List(
graphene.String, ProductType,
hub_uuid=graphene.String(required=True), description="Get unique products from all offers",
description="Find unique product UUIDs that can be delivered to this hub",
) )
find_hubs_for_product = graphene.List( offers_by_product = graphene.List(
NodeType, OfferNodeType,
product_uuid=graphene.String(required=True), product_uuid=graphene.String(required=True),
description="Find logistics hubs where this product can be delivered", description="Get all offers for a product",
) )
find_supplier_product_hubs = graphene.List( hubs_near_offer = graphene.List(
NodeType, NodeType,
offer_uuid=graphene.String(required=True),
limit=graphene.Int(default_value=12),
description="Get nearest hubs to an offer location",
)
suppliers = graphene.List(
SupplierType,
description="Get unique suppliers from all offers",
)
products_by_supplier = graphene.List(
ProductType,
supplier_uuid=graphene.String(required=True),
description="Get products offered by a supplier",
)
offers_by_supplier_product = graphene.List(
OfferNodeType,
supplier_uuid=graphene.String(required=True), supplier_uuid=graphene.String(required=True),
product_uuid=graphene.String(required=True), product_uuid=graphene.String(required=True),
description="Find hubs where this supplier can deliver this product", description="Get offers from a supplier for a specific product",
) )
find_offers_for_hub_by_product = graphene.List( products_near_hub = graphene.List(
ProductType,
hub_uuid=graphene.String(required=True),
radius_km=graphene.Float(default_value=500),
description="Get products available near a hub",
)
offers_to_hub = graphene.List(
ProductRouteOptionType, ProductRouteOptionType,
hub_uuid=graphene.String(required=True), hub_uuid=graphene.String(required=True),
product_uuid=graphene.String(required=True), product_uuid=graphene.String(required=True),
limit_sources=graphene.Int(default_value=10), limit=graphene.Int(default_value=10),
description="Find product offers that can be delivered to hub (with routes)", description="Get offers for a product with routes to hub",
)
delivery_to_hub = graphene.Field(
ProductRouteOptionType,
offer_uuid=graphene.String(required=True),
hub_uuid=graphene.String(required=True),
description="Get delivery route from offer to hub",
) )
@staticmethod @staticmethod
@@ -796,180 +854,258 @@ class Query(graphene.ObjectType):
clusters = get_clustered_nodes(db, west, south, east, north, zoom, transport_type) clusters = get_clustered_nodes(db, west, south, east, north, zoom, transport_type)
return [ClusterPointType(**c) for c in clusters] return [ClusterPointType(**c) for c in clusters]
def resolve_find_products_for_hub(self, info, hub_uuid): def resolve_products(self, info):
""" """Get unique products from all offers."""
Find unique product UUIDs that can be delivered to this hub.
Uses reverse traversal from hub to find reachable offer nodes.
"""
db = get_db() db = get_db()
ensure_graph() aql = """
FOR node IN nodes
FILTER node.node_type == 'offer'
FILTER node.product_uuid != null
COLLECT product_uuid = node.product_uuid INTO offers
LET first_offer = FIRST(offers).node
RETURN {
uuid: product_uuid,
name: first_offer.product_name
}
"""
try:
cursor = db.aql.execute(aql)
products = [ProductType(uuid=p['uuid'], name=p.get('name')) for p in cursor]
logger.info("Found %d unique products", len(products))
return products
except Exception as e:
logger.error("Error getting products: %s", e)
return []
# Check hub exists def resolve_offers_by_product(self, info, product_uuid):
"""Get all offers for a product."""
db = get_db()
aql = """
FOR node IN nodes
FILTER node.node_type == 'offer'
FILTER node.product_uuid == @product_uuid
RETURN node
"""
try:
cursor = db.aql.execute(aql, bind_vars={'product_uuid': product_uuid})
offers = []
for node in cursor:
offers.append(OfferNodeType(
uuid=node['_key'],
product_uuid=node.get('product_uuid'),
product_name=node.get('product_name'),
supplier_uuid=node.get('supplier_uuid'),
latitude=node.get('latitude'),
longitude=node.get('longitude'),
country=node.get('country'),
country_code=node.get('country_code'),
price_per_unit=node.get('price_per_unit'),
currency=node.get('currency'),
quantity=node.get('quantity'),
unit=node.get('unit'),
))
logger.info("Found %d offers for product %s", len(offers), product_uuid)
return offers
except Exception as e:
logger.error("Error getting offers by product: %s", e)
return []
def resolve_hubs_near_offer(self, info, offer_uuid, limit=12):
"""Get nearest hubs to an offer location."""
db = get_db()
nodes_col = db.collection('nodes')
offer = nodes_col.get(offer_uuid)
if not offer:
logger.info("Offer %s not found", offer_uuid)
return []
lat = offer.get('latitude')
lon = offer.get('longitude')
if lat is None or lon is None:
logger.info("Offer %s has no coordinates", offer_uuid)
return []
aql = """
FOR node IN nodes
FILTER node.node_type == 'logistics' OR node.node_type == null
FILTER node.product_uuid == null
FILTER node.latitude != null AND node.longitude != null
LET dist = DISTANCE(node.latitude, node.longitude, @lat, @lon) / 1000
SORT dist ASC
LIMIT @limit
RETURN MERGE(node, {distance_km: dist})
"""
try:
cursor = db.aql.execute(aql, bind_vars={'lat': lat, 'lon': lon, 'limit': limit})
hubs = []
for node in cursor:
hubs.append(NodeType(
uuid=node['_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=[],
))
logger.info("Found %d hubs near offer %s", len(hubs), offer_uuid)
return hubs
except Exception as e:
logger.error("Error getting hubs near offer: %s", e)
return []
def resolve_suppliers(self, info):
"""Get unique suppliers from all offers."""
db = get_db()
aql = """
FOR node IN nodes
FILTER node.node_type == 'offer'
FILTER node.supplier_uuid != null
COLLECT supplier_uuid = node.supplier_uuid
RETURN { uuid: supplier_uuid }
"""
try:
cursor = db.aql.execute(aql)
suppliers = [SupplierType(uuid=s['uuid']) for s in cursor]
logger.info("Found %d unique suppliers", len(suppliers))
return suppliers
except Exception as e:
logger.error("Error getting suppliers: %s", e)
return []
def resolve_products_by_supplier(self, info, supplier_uuid):
"""Get products offered by a supplier."""
db = get_db()
aql = """
FOR node IN nodes
FILTER node.node_type == 'offer'
FILTER node.supplier_uuid == @supplier_uuid
FILTER node.product_uuid != null
COLLECT product_uuid = node.product_uuid INTO offers
LET first_offer = FIRST(offers).node
RETURN {
uuid: product_uuid,
name: first_offer.product_name
}
"""
try:
cursor = db.aql.execute(aql, bind_vars={'supplier_uuid': supplier_uuid})
products = [ProductType(uuid=p['uuid'], name=p.get('name')) for p in cursor]
logger.info("Found %d products for supplier %s", len(products), supplier_uuid)
return products
except Exception as e:
logger.error("Error getting products by supplier: %s", e)
return []
def resolve_offers_by_supplier_product(self, info, supplier_uuid, product_uuid):
"""Get offers from a supplier for a specific product."""
db = get_db()
aql = """
FOR node IN nodes
FILTER node.node_type == 'offer'
FILTER node.supplier_uuid == @supplier_uuid
FILTER node.product_uuid == @product_uuid
RETURN node
"""
try:
cursor = db.aql.execute(aql, bind_vars={
'supplier_uuid': supplier_uuid,
'product_uuid': product_uuid
})
offers = []
for node in cursor:
offers.append(OfferNodeType(
uuid=node['_key'],
product_uuid=node.get('product_uuid'),
product_name=node.get('product_name'),
supplier_uuid=node.get('supplier_uuid'),
latitude=node.get('latitude'),
longitude=node.get('longitude'),
country=node.get('country'),
country_code=node.get('country_code'),
price_per_unit=node.get('price_per_unit'),
currency=node.get('currency'),
quantity=node.get('quantity'),
unit=node.get('unit'),
))
logger.info("Found %d offers for supplier %s product %s", len(offers), supplier_uuid, product_uuid)
return offers
except Exception as e:
logger.error("Error getting offers by supplier product: %s", e)
return []
def resolve_products_near_hub(self, info, hub_uuid, radius_km=500):
"""Get products available near a hub (within radius)."""
db = get_db()
nodes_col = db.collection('nodes') nodes_col = db.collection('nodes')
hub = nodes_col.get(hub_uuid) hub = nodes_col.get(hub_uuid)
if not hub: if not hub:
logger.info("Hub %s not found", hub_uuid) logger.info("Hub %s not found", hub_uuid)
return [] return []
# Find all offer nodes reachable from hub via graph traversal lat = hub.get('latitude')
# Offer nodes have product_uuid field lon = hub.get('longitude')
if lat is None or lon is None:
logger.info("Hub %s has no coordinates", hub_uuid)
return []
aql = """ aql = """
FOR v, e, p IN 1..10 ANY @hub_id GRAPH 'optovia_graph'
FILTER v.product_uuid != null
COLLECT product_uuid = v.product_uuid
RETURN product_uuid
"""
try:
cursor = db.aql.execute(
aql,
bind_vars={'hub_id': f'nodes/{hub_uuid}'},
)
product_uuids = list(cursor)
logger.info("Found %d products for hub %s", len(product_uuids), hub_uuid)
return product_uuids
except Exception as e:
logger.error("Error finding products for hub: %s", e)
return []
def resolve_find_hubs_for_product(self, info, product_uuid):
"""
Find logistics hubs where this product can be delivered.
Finds offer nodes with this product, then finds reachable hubs.
"""
db = get_db()
ensure_graph()
# Find all offer nodes with this product_uuid
aql_offers = """
FOR node IN nodes FOR node IN nodes
FILTER node.product_uuid == @product_uuid FILTER node.node_type == 'offer'
RETURN node._key FILTER node.product_uuid != null
FILTER node.latitude != null AND node.longitude != null
LET dist = DISTANCE(node.latitude, node.longitude, @lat, @lon) / 1000
FILTER dist <= @radius_km
COLLECT product_uuid = node.product_uuid INTO offers
LET first_offer = FIRST(offers).node
RETURN {
uuid: product_uuid,
name: first_offer.product_name
}
""" """
try: try:
cursor = db.aql.execute(aql_offers, bind_vars={'product_uuid': product_uuid}) cursor = db.aql.execute(aql, bind_vars={'lat': lat, 'lon': lon, 'radius_km': radius_km})
offer_keys = list(cursor) products = [ProductType(uuid=p['uuid'], name=p.get('name')) for p in cursor]
logger.info("Found %d products near hub %s", len(products), hub_uuid)
return products
except Exception as e: except Exception as e:
logger.error("Error finding offers for product: %s", e) logger.error("Error getting products near hub: %s", e)
return [] return []
if not offer_keys: def resolve_offers_to_hub(self, info, hub_uuid, product_uuid, limit=10):
logger.info("No offers found for product %s", product_uuid) """Get offers for a product with routes to hub."""
return []
# Find hubs reachable from these offer nodes
aql_hubs = """
LET offer_ids = @offer_ids
FOR offer_id IN offer_ids
FOR v, e, p IN 1..10 ANY CONCAT('nodes/', offer_id) GRAPH 'optovia_graph'
FILTER (v.node_type == 'logistics' OR v.node_type == null)
FILTER v.product_uuid == null
COLLECT hub_key = v._key INTO hubs
LET hub = FIRST(hubs).v
RETURN hub
"""
try:
cursor = db.aql.execute(aql_hubs, bind_vars={'offer_ids': offer_keys})
hubs = list(cursor)
result = []
for hub in hubs:
if hub:
result.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'),
synced_at=hub.get('synced_at'),
transport_types=hub.get('transport_types') or [],
edges=[],
))
logger.info("Found %d hubs for product %s", len(result), product_uuid)
return result
except Exception as e:
logger.error("Error finding hubs for product: %s", e)
return []
def resolve_find_supplier_product_hubs(self, info, supplier_uuid, product_uuid):
"""
Find hubs where this supplier can deliver this product.
Finds offer nodes matching both supplier and product, then finds reachable hubs.
"""
db = get_db()
ensure_graph()
# Find offer nodes with this supplier_uuid AND product_uuid
aql_offers = """
FOR node IN nodes
FILTER node.product_uuid == @product_uuid
FILTER node.supplier_uuid == @supplier_uuid
RETURN node._key
"""
try:
cursor = db.aql.execute(
aql_offers,
bind_vars={'product_uuid': product_uuid, 'supplier_uuid': supplier_uuid}
)
offer_keys = list(cursor)
except Exception as e:
logger.error("Error finding supplier offers: %s", e)
return []
if not offer_keys:
logger.info("No offers found for supplier %s, product %s", supplier_uuid, product_uuid)
return []
# Find hubs reachable from these offer nodes
aql_hubs = """
LET offer_ids = @offer_ids
FOR offer_id IN offer_ids
FOR v, e, p IN 1..10 ANY CONCAT('nodes/', offer_id) GRAPH 'optovia_graph'
FILTER (v.node_type == 'logistics' OR v.node_type == null)
FILTER v.product_uuid == null
COLLECT hub_key = v._key INTO hubs
LET hub = FIRST(hubs).v
RETURN hub
"""
try:
cursor = db.aql.execute(aql_hubs, bind_vars={'offer_ids': offer_keys})
hubs = list(cursor)
result = []
for hub in hubs:
if hub:
result.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'),
synced_at=hub.get('synced_at'),
transport_types=hub.get('transport_types') or [],
edges=[],
))
logger.info("Found %d hubs for supplier %s product %s", len(result), supplier_uuid, product_uuid)
return result
except Exception as e:
logger.error("Error finding supplier product hubs: %s", e)
return []
def resolve_find_offers_for_hub_by_product(self, info, hub_uuid, product_uuid, limit_sources=10):
"""
Find product offers that can be delivered to hub (with routes).
Same as find_product_routes but with business-oriented naming.
"""
return self.resolve_find_product_routes( return self.resolve_find_product_routes(
info, info,
product_uuid=product_uuid, product_uuid=product_uuid,
to_uuid=hub_uuid, to_uuid=hub_uuid,
limit_sources=limit_sources, limit_sources=limit,
limit_routes=1, limit_routes=1,
) )
def resolve_delivery_to_hub(self, info, offer_uuid, hub_uuid):
"""Get delivery route from offer to hub."""
db = get_db()
nodes_col = db.collection('nodes')
offer = nodes_col.get(offer_uuid)
if not offer:
logger.info("Offer %s not found", offer_uuid)
return None
routes = self._build_routes(db, offer_uuid, hub_uuid, limit=1)
if not routes:
return None
return ProductRouteOptionType(
source_uuid=offer_uuid,
source_name=offer.get('name'),
source_lat=offer.get('latitude'),
source_lon=offer.get('longitude'),
distance_km=routes[0].total_distance_km if routes else None,
routes=routes,
)
schema = graphene.Schema(query=Query) schema = graphene.Schema(query=Query)