caso de uso: Vista única
Industrias: Servicios financieros
Productos: MongoDB Atlas
Descripción general de la solución
Los delincuentes financieros operan en redes. El lavado de dinero, las redes de fraude y la evasión de sanciones dependen de redes de empresas shell, directores fiduciarios y rutas interpuestas de transacciones. Los equipos de cumplimiento deben trazar y analizar la red de relaciones en torno a una entidad sospechosa.
Los enfoques tradicionales se enfrentan a dos cuellos de botella:
Consultas costosas: los sistemas de gestión de bases de datos relacionales utilizan expresiones comunes de tabla recursivas (CTE). Estas consultas se vuelven costosas después de dos o tres saltos.
Idas y vueltas constantes de la red: El procesamiento del grafo en el lado del cliente extrae datos hasta la capa de la aplicación. El cliente ejecuta una nueva query para cada salto.
En esta solución, tú:
Cree un motor de análisis de red que utilice operadores de MongoDB para evitar estos cuellos de botella.
Ejecuta la búsqueda de grafos de múltiples saltos, detección de ruta más corta, puntuación de centralidad, detección de Community y propagación de riesgos directamente dentro del marco de agregación de MongoDB.
Esta solución se centra en el motor de análisis de red y los operadores de MongoDB que lo potencian. Para implementar la arquitectura general con MongoDB Atlas, FastAPI y Next.js, también se debe seguir la anterior Solutions Library, Mitigación de delitos financieros.
¿Por qué MongoDB en lugar de una base de datos de grafos dedicada?
Almacene entidades y relaciones en MongoDB junto con índices de búsqueda, vector embeddings y flujos de cambios.
Evita sincronizar datos entre dos sistemas, gestionar infraestructura extra y sufrir latencia entre sistemas porque mantienes cargas de trabajo de grafo en MongoDB.
Utiliza el operador
$graphLookupde MongoDB para ejecutar una búsqueda en amplitud con queries indexados estándar en cada salto, de modo que el rendimiento dependa de índices y no de un motor de grafo separado.Para los recorridos de 1a5saltos, comunes en las investigaciones de cumplimiento, este enfoque iguala las bases de datos de grafos dedicadas al consultar los mismos datos que tu aplicación ya lee y escribe.
Nota
En las investigaciones de cumplimiento, generalmente se rastrean algunos pasos desde un tema. Las contrapartes inmediatas y los intermediarios de primer nivel se encuentran a 1–2 saltos de distancia, mientras que las estructuras shell y las rutas de movimiento de dinero generalmente se ubican dentro de 3–5 saltos. Más allá de eso, los enlaces se vuelven menos significativos y más difíciles de explicar, por lo que la mayoría de las investigaciones prácticas se mantienen en este rango de 1a5saltos.
Arquitecturas de Referencia
El motor de análisis de red se encuentra entre la capa de servicio FastAPI y MongoDB Atlas y procesa operaciones de grafos a través de la pipeline de agregación.
Figura 1. Pipeline de Investigación Agente
Dual $graphLookup Patrón
Las redes de blanqueo de capitales implican flujos bidireccionales. Una entidad puede obtener fondos en una relación y recibirlos en otra.
Un solo $graphLookup recorre los bordes en una sola dirección. Esta solución ejecuta dos búsquedas paralelas — directa y reversa — y fusiona los resultados en un grafo de red unificado.
pipeline = [ {"$match": {"entityId": center_entity_id}}, # Forward traversal: follow source → target edges {"$graphLookup": { "from": "relationships", "startWith": "$entityId", "connectFromField": "target.entityId", "connectToField": "source.entityId", "as": "forward_relationships", "maxDepth": max_depth - 1, "restrictSearchWithMatch": { "active": True, "confidence": {"$gte": min_confidence} } }}, # Reverse traversal: follow target → source edges {"$graphLookup": { "from": "relationships", "startWith": "$entityId", "connectFromField": "source.entityId", "connectToField": "target.entityId", "as": "reverse_relationships", "maxDepth": max_depth - 1, "restrictSearchWithMatch": { "active": True, "confidence": {"$gte": min_confidence} } }}, # Merge both directions {"$project": { "entityId": 1, "all_relationships": { "$concatArrays": [ "$forward_relationships", "$reverse_relationships" ] } }}, {"$unwind": "$all_relationships"}, {"$replaceRoot": {"newRoot": "$all_relationships"}}, {"$limit": max_relationships} ]
Figura 2. $graphLookup dual: descubrimiento de redes bidireccional
La aplicación recibe el grafo de red completo en una sola ida y vuelta. Ambos recorridos se ejecutan en una única canalización de agregación, lo que elimina el problema de consultas N+1 al evitar la emisión de una consulta de seguimiento separada por cada document devuelto en la consulta inicial.
Cómo $graphLookup Recorre el grafo
$graphLookup realiza una búsqueda en anchura (BFS) en oleadas discretas:
Semilla: Evalúa
startWithsegún el input document. Los valores de un arreglo inician la frontera simultáneamente (BFS de múltiples raíces).query: Construir
{ connectToField: { $in: [frontier_values] }}, fusionado con cualquier filtrorestrictSearchWithMatch. Ejecuta como una query indexada estándar en la colección de origen.Expandirse: Para cada document coincidente que no esté en el conjunto visitado, añádalo a los resultados, extraiga los valores de
connectFromFieldy empújelos al siguiente límite.Repetir: Incrementar la profundidad. Vuelve al paso 2 hasta que la frontera se vacíe o se alcance
maxDepth.Ensamblar: coloque todos los accumulated documents en un arreglo bajo el campo as.
La detección de ciclos es automática. Un conjunto interno de elementos visitados previene bucles infinitos en grafos cíclicos (A → B → C → A), que son comunes en estructuras de lavado de dinero. Cada document aparece en los resultados exactamente una vez.
La restrictSearchWithMatch Ventaja
restrictSearchWithMatch empuja los criterios de filtro dentro del propio recorrido, no como un post-filtro. MongoDB poda ramas muertas durante el recorrido en lugar de descubrir el grafo completo y filtrar posteriormente. Para redes grandes, esto puede reducir el conjunto de trabajo en un orden de magnitud.
Figura 3. Filtro durante el recorrido vs. post-filtrado
Enfoque de modelo de datos
La solución utiliza dos colecciones: entities y relationships. Siguen un patrón de lista de adyacencia. Los metadatos de borde (nivel de confianza, evidencia, estado de verificación) se establecen como campos de primera clase en el document de relación.
Esquema de relaciones
{ "relationshipId": "REL_8910", "source": { "entityId": "ENT_123", "entityType": "individual" }, "target": { "entityId": "ENT_456", "entityType": "organization" }, "type": "beneficial_owner_of", "direction": "directed", "strength": 0.85, "confidence": 0.95, "active": true, "verified": true, "evidence": [ { "evidence_type": "corporate_registry", "confidence": 0.95, "source": "Companies House UK" } ], "datasource": "KYC_onboarding" }
Decisiones clave de diseño
Referencias anidadas de fuente/destino: La estructura
source.entityIdytarget.entityIdse asigna directamente a los parámetrosconnectFromFieldyconnectToFieldde$graphLookup. También conserva los metadatos del tipo de entidad a nivel de borde sin requerir una unión.separado
strengthyconfidencecampos: La fortaleza refleja qué tan sólido es la relación, como en el caso de un UBO que posee 90% de participación en comparación con un socio comercial distante. La confianza refleja la fiabilidad en el punto de datos verificado por dos fuentes independientes, en comparación con los inferidos a partir de una dirección compartida. La propagación del riesgo utiliza ambos valores de manera diferente.activebooleano para borrado suave: los sistemas AML requieren registros de auditoría. Elimina una relación configurandoactive: falseen lugar de remover el document. Esta bandera también sirve como un filtro de recorrido enrestrictSearchWithMatch.
Compilar la solución
Clona el repositorio y sigue las instrucciones de configuración en el README de GitHub:
git clone https://github.com/mongodb-industry-solutions/fsi-aml-fraud-detection.git cd fsi-aml-fraud-detection/aml-backend poetry install poetry run uvicorn main:app --host 0.0.0.0 --port 8001 --reload
Los siguientes subapartados recorren las seis operaciones básicas de gráficos implementadas en el NetworkRepository.
Crear índices requeridos
$graphLookupemite una consulta{ connectToField: { $in: [frontier] } }en cada ola de BFS. ÍndiceconnectToFielden ambas direcciones de recorrido:
db.relationships.createIndex({ "source.entityId": 1, "active": 1, "confidence": -1 }); db.relationships.createIndex({ "target.entityId": 1, "active": 1, "confidence": -1 });
La ventaja del índice es más fuerte en 1–3 saltos y disminuye a medida que aumenta la profundidad. La investigación AML en saltos de 1a4encaja perfectamente en el punto óptimo.
Desarrollar redes de entidades (Dual $graphLookup)
Utilice el pipeline dual
$graphLookupdescrito en Arquitecturas de Referencia para construir redes bidireccionales completas en una única agregación.El endpoint
/network/{entity_id}expone esta operación con profundidad configurable, umbral de confianza y recuento máximo de nodos.
Buscar rutas más cortas (depthField)
Determine si un cliente de bajo riesgo se conecta a una entidad sancionada — y reconstruya la cadena exacta.
Utiliza
depthFieldpara anotar cada relación descubierta con su distancia de salto:
pipeline = [ {"$match": {"entityId": source_entity_id}}, {"$graphLookup": { "from": "relationships", "startWith": "$entityId", "connectFromField": "source.entityId", "connectToField": "target.entityId", "as": "forward_paths", "maxDepth": max_depth - 1, "depthField": "depth" }}, {"$graphLookup": { "from": "relationships", "startWith": "$entityId", "connectFromField": "target.entityId", "connectToField": "source.entityId", "as": "reverse_paths", "maxDepth": max_depth - 1, "depthField": "depth" }}, {"$project": { "all_paths": {"$concatArrays": ["$forward_paths", "$reverse_paths"]} }}, {"$unwind": "$all_paths"}, {"$match": {"$or": [ {"all_paths.source.entityId": target_entity_id}, {"all_paths.target.entityId": target_entity_id} ]}}, {"$sort": {"all_paths.depth": 1}}, {"$limit": 1} ]
El resultado identifica la menor profundidad. Un segundo $graphLookup acotado a esa profundidad reconstruye toda la cadena de relaciones:
Customer A → [beneficial_owner_of] → Shell Corp B → [director_of] → Sanctioned Entity C
Calcular estadísticas de red ($facet)
- Use
$facetpara ejecutar cinco análisis paralelos en el mismo conjunto de entidades en un único pipeline: distribución de riesgo, desglose por tipo de entidad, detección de hubs, calificación de prominencia y métricas básicas:
stats_pipeline = [ {"$match": {"entityId": {"$in": network_entity_ids}}}, {"$addFields": { "connection_count": {"$size": {"$ifNull": ["$connected_entities", []]}} }}, {"$facet": { "basic_stats": [{"$group": { "_id": None, "total_nodes": {"$sum": 1}, "avg_risk_score": {"$avg": "$riskAssessment.overall.score"}, "max_risk_score": {"$max": "$riskAssessment.overall.score"} }}], "risk_distribution": [ {"$group": {"_id": "$riskAssessment.overall.level", "count": {"$sum": 1}}}, {"$sort": {"_id": 1}} ], "hub_entities": [ {"$match": {"connection_count": {"$gte": 2}}}, {"$sort": {"connection_count": -1}}, {"$limit": 5}, {"$project": {"entityId": 1, "name": 1, "connection_count": 1}} ] }} ]
Sin $facet, cada análisis requiere una pipeline separada. $facet procesa todas las sub-pipelines en paralelo y devuelve los resultados en una única respuesta. Esto se ejecuta en 2-5 milisegundos.
Centralidad de puntuación ($switch para ponderación específica del dominio)
Identifica las entidades que conectan a los actores más sospechosos.
El pipeline de centralidad utiliza
$facetpara agregar las conexiones salientes y entrantes por separado, luego fusiona y puntúa.El operador
$switchasigna pesos de riesgo a cada tipo de relación directamente dentro de la agregación:
"outgoing_risk_weighted": { "$sum": {"$multiply": [ "$confidence", {"$switch": { "branches": [ {"case": {"$in": ["$type", [ "confirmed_same_entity", "business_associate_suspected" ]]}, "then": 0.9}, {"case": {"$in": ["$type", [ "director_of", "ubo_of", "parent_of_subsidiary" ]]}, "then": 0.7}, {"case": {"$in": ["$type", [ "household_member", "professional_colleague_public" ]]}, "then": 0.3} ], "default": 0.5 }} ]} }
Una conexión
confirmed_same_entitycontribuye mucho más a la centralidad ponderada por el riesgo que un enlacehousehold_member.La puntuación compuesta final combina la centralidad del grado normalizada (40%), el peso promedio de confianza (30%) y la centralidad ponderada por el riesgo (30%); todo calculado en el lado del servidor.
Detectar comunidades y propagar riesgos
- La detección de Community construye un mapa de adyacencia mediante la agregación y filtra en
confidence >= 0.7para dibujar límites comunitarios solo alrededor de relaciones de alta confianza:
adjacency_pipeline = [ {"$match": { "$or": [ {"source.entityId": {"$in": entity_ids}}, {"target.entityId": {"$in": entity_ids}} ], "active": True, "confidence": {"$gte": 0.7} }}, {"$group": { "_id": "$source.entityId", "connections": {"$addToSet": "$target.entityId"} }} ]
$addToSetdeduplica las conexiones automáticamente: dos entidades que comparten tantoshared_addresscomobusiness_associaterelación aparecen como una sola conexión.
La propagación del riesgo aplica una decadencia exponencial a través de la cadena de relaciones después de que $graphLookup descubre la red:
propagated_risk = ( parent_entity_risk * propagation_factor # decay per hop (default: 0.5) * relationship_confidence # trust level for this edge * type_risk_weight # domain-specific weight )
Una shell empresa de una entidad sancionada (alta confianza, tipo de relación de alto riesgo) recibe casi la puntuación total de riesgo. La conexión en redes de medios sociales de su contador recibe casi nada. El recorrido es de búsqueda primero en anchura, limitado en profundidad a 3 saltos, y se detiene cuando la puntuación propagada cae por debajo de un umbral configurable.
Lecciones clave
Utilizar dual
$graphLookuppara el descubrimiento bidireccional de la red: Ejecuta dos búsquedas paralelas, directa e inversa, y fusiona los resultados con$concatArrayspara capturar toda la red en una sola agregación.Empuje los filtros hacia la navegación con
restrictSearchWithMatchPrune ramas muertas durante BFS en vez de filtrar el grafo completo después para reducir el conjunto de trabajo en redes grandes.Crear índices compuestos en
connectToFieldy sus filtros de recorrido:$graphLookupemite una query de$match/$inen cada ola de BFS, por lo que indexarconnectToFieldjunto conactiveyconfidenceevita exploraciones de colección en cada paso.Calcula análisis de red del lado del servidor con
$facety$switch: Ejecute la distribución de riesgos, la detección de hubs y la puntuación de centralidad en subprocesos paralelos, y utilice$switchpara asignar ponderaciones de riesgos específicas del dominio a los tipos de relaciones dentro de la agregación.Aplica la atenuación exponencial para la propagación del riesgo: Combina el descubrimiento de red basado en
$graphLookupcon una fórmula de atenuación por salto que incluya la confianza y el tipo de relación para que el model distinga automáticamente conexiones estructurales de alto riesgo de los enlaces sociales de bajo riesgo. Esto coincide con la directriz de mantener los aprendizajes clave en un estilo imperativo coherente.
Autores
Luis Pazmiño
Mehar Grewal
Andrea Alaman Calderon