Para agentes de IA: hay un índice de documentación disponible en https://www.mongodb.com/es/docs/llms.txt — versiones en markdown de todas las páginas están disponibles agregando .md a cualquier ruta URL.
Docs Menu

Análisis de la red de AML con $graphLookup

caso de uso: Vista única

Industrias: Servicios financieros

Productos: MongoDB Atlas

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.

  • 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 $graphLookup de 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.

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.

pipeline de Investigación Agéntica

Figura 1. Pipeline de Investigación Agente

haga clic para ampliar

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}
]
Dual $graphLookup — Descubrimiento de redes bidireccional

Figura 2. $graphLookup dual: descubrimiento de redes bidireccional

haga clic para ampliar

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 startWith segú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 filtro restrictSearchWithMatch. 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 connectFromField y 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.

Filtro durante la recorrida vs posfiltrado

Figura 3. Filtro durante el recorrido vs. post-filtrado

haga clic para ampliar

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"
}
  • Referencias anidadas de fuente/destino: La estructura source.entityId y target.entityId se asigna directamente a los parámetros connectFromField y connectToField de $graphLookup. También conserva los metadatos del tipo de entidad a nivel de borde sin requerir una unión.

  • separado strength y confidence campos: 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.

  • active booleano para borrado suave: los sistemas AML requieren registros de auditoría. Elimina una relación configurando active: false en lugar de remover el document. Esta bandera también sirve como un filtro de recorrido en restrictSearchWithMatch.

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.

1
  • $graphLookup emite una consulta { connectToField: { $in: [frontier] } } en cada ola de BFS. Índice connectToField en 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.

2
  • Utilice el pipeline dual $graphLookup descrito 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.

3
  • Determine si un cliente de bajo riesgo se conecta a una entidad sancionada — y reconstruya la cadena exacta.

  • Utiliza depthField para 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
4
  • Use $facet para 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.

5
  • Identifica las entidades que conectan a los actores más sospechosos.

  • El pipeline de centralidad utiliza $facet para agregar las conexiones salientes y entrantes por separado, luego fusiona y puntúa.

  • El operador $switch asigna 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_entity contribuye mucho más a la centralidad ponderada por el riesgo que un enlace household_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.

6
  • La detección de Community construye un mapa de adyacencia mediante la agregación y filtra en confidence >= 0.7 para 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"}
}}
]
  • $addToSet deduplica las conexiones automáticamente: dos entidades que comparten tanto shared_address como business_associate relació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.

  • Utilizar dual $graphLookup para el descubrimiento bidireccional de la red: Ejecuta dos búsquedas paralelas, directa e inversa, y fusiona los resultados con $concatArrays para 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 connectToField y sus filtros de recorrido: $graphLookup emite una query de $match/$in en cada ola de BFS, por lo que indexar connectToField junto con active y confidence evita exploraciones de colección en cada paso.

  • Calcula análisis de red del lado del servidor con $facet y $switch: Ejecute la distribución de riesgos, la detección de hubs y la puntuación de centralidad en subprocesos paralelos, y utilice $switch para 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.

  • Luis Pazmiño

  • Mehar Grewal

  • Andrea Alaman Calderon