Capítulo 4. Explorar los barrios en desarrollo
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Para pasar a la siguiente fase en el desarrollo de aplicaciones gráficas, vamos a basarnos en la sencilla aplicación Cliente 360 (C360) del Capítulo 3. Añadiremos algunas capas más, o barrios, a ese ejemplo para ilustrar la siguiente oleada de conceptos en el pensamiento gráfico.
Añadir datos a nuestro ejemplo proporciona una imagen más realista de la complejidad del modelado de datos, la consulta y la aplicación del pensamiento gráfico a nuestros datos financieros centrados en el cliente.
Consideramos que la transición del ejemplo básico del Capítulo 3 a la complejidad de este capítulo es análoga a los pasos del proceso de aprender a bucear. Lo que hicimos en el Capítulo 3 fue como empezar a aprender a bucear en una piscina de vadeo; no está muy claro qué sentido tiene cuando estás en un agua tan poco profunda. Pero necesitábamos empezar desde un lugar conocido. Los ejemplos de este capítulo son como bucear en una piscina profunda. Después, estaremos preparados para adentrarnos en profundidades más interesantes en el Capítulo 5.
Avance de capítulo: Construir un cliente 360 más realista
En este capítulo hay tres secciones principales.
En la primera sección, exploraremos y explicaremos el pensamiento gráfico para presentar las buenas prácticas en el modelado de datos gráficos. Para ello, añadiremos más barrios de datos a nuestro ejemplo C360, de modo que podamos responder a las siguientes preguntas:
-
¿Cuáles son las 20 transacciones más recientes de la cuenta de Miguel?
-
En diciembre, ¿en qué vendedores compró Michael y con qué frecuencia?
-
Encuentra y actualiza las transacciones que más valoran Jamie y Aaliyah: los pagos de su cuenta a su préstamo hipotecario. (La consulta 3 es un ejemplo de personalización).
A lo largo de esta sección inicial, seguiremos el diseño basado en consultas para ilustrar las buenas prácticas habituales en la creación de un modelo de datos de grafos de propiedades. Los temas incluyen la asignación de tus datos a vértices o perímetros, el tiempo de modelado y los errores más comunes.
En la siguiente sección, construiremos consultas Gremlin más profundas. Estas consultas recorren tres, cuatro y cinco vecindarios de datos. También explicaremos cómo utilizar las propiedades para cortar, ordenar y recorrer los datos del gráfico, y hablaremos de las consultas en ventanas de tiempo. Al final de esta sección, habremos ilustrado todos los datos, conceptos técnicos y modelado de datos que planeamos para nuestro ejemplo.
Terminaremos el capítulo revisando las consultas básicas para introducir algunas técnicas de consulta más avanzadas. Estas técnicas suelen consistir en intentar dar formato a los resultados de tus consultas en una estructura más fácil de usar.
Este contenido nos prepara para presentar el esquema final, con calidad de producción, de este ejemplo, lo que haremos en el capítulo 5.
Modelado gráfico de datos 101
Durante los primeros días de trabajo con bases de datos de grafos respaldadas por Apache Cassandra, mi equipo estaba sentado alrededor de los sofás en el salón de nuestra startup respaldada por una empresa de capital riesgo. Estábamos diseñando un modelo de datos de grafos para almacenar datos sanitarios en una base de datos de grafos.
Rápidamente acordamos que los médicos, los pacientes y los hospitales eran nuestras principales entidades de importancia, y por tanto serían vértices. Todo lo demás, después de eso, era un debate. Vértices, perímetros, propiedades y nombres: todo el mundo tenía una opinión defendible sobre todo. Nuestros desacuerdos más memorables fueron polarizantes. ¿Cómo deberíamos llamar a los perímetros entre médicos y pacientes? Todas estas entidades viven o trabajan en algún lugar; ¿cómo modelamos las direcciones? ¿Es el país un vértice o una propiedad, o debe quedar fuera de nuestro modelo?
Fue una conversación difícil. Tardamos mucho más de lo que esperábamos en llegar a un consenso sobre el diseño, y ninguno de nosotros se sintió realmente cómodo con él.
Desde aquella sesión de diseño, cada vez que asesoro a un equipo gráfico de todo el mundo, puedo sentir tensiones similares y ver consensos de diseño parecidos. Las tensiones son siempre reales, siempre están ahí y siempre son observables.
Esta sección trata de ayudar a tu equipo a mantener un debate más constructivo sobre tu modelo de datos de gráficos. Para lograrlo, queremos recorrer tres secciones de consejos para crear un buen modelo de datos gráficos. Esas secciones de consejos serán:
-
¿Debe ser un vértice o un perímetro?
-
¿Ya te has perdido? Guíame por la dirección.
-
Un gráfico no tiene nombre: Errores comunes al nombrar
Hemos seleccionado estos temas por dos razones. En primer lugar, estos temas cubren la mayoría de los puntos conflictivos que encontrarás durante el proceso de modelado. En segundo lugar, estos temas apoyan el punto en el que nos encontramos en el desarrollo del ejemplo en marcha para estos capítulos. Los detalles para consejos de modelado más profundos y avanzados se introducirán cuando lleguemos allí.
¿Debe ser un vértice o un perímetro?
Éste es el tema más discutible sobre el modelado de grafos de propiedades. De en medio de los debates más acalorados, hemos sacado una serie de consejos para crear modelos de datos de grafos.
Empecemos nuestros consejos por el principio. En nuestro mundo, el principio es donde quieres empezar tus recorridos por el grafo.
Regla de oro nº 1
Si quieres iniciar tu recorrido en algún dato, haz que ese dato sea un vértice.
Para desentrañar nuestro primer consejo, volvamos a una de las consultas que construimos en el Capítulo 3:
Which accounts does this customer own?
Hay tres datos necesarios para responder a esa pregunta: clientes, cuentas y una conexión de qué cliente es propietario de una cuenta. Piensa cómo podrías utilizar esos datos para "encontrar todas las cuentas propiedad de Miguel". Hay dos formas de traducir esta afirmación en una consulta a la base de datos: "Miguel es propietario de cuentas" o "cuentas propiedad de Miguel".
Hablemos de la primera opción: empezar con Miguel para encontrar sus cuentas. Esto significa que empiezas con datos sobre personas, en concreto, con los datos sobre Miguel. En tu cabeza, cuando encuentres un punto de partida para una consulta, querrás traducir esos datos para que sean una etiqueta de vértice en tu modelo de grafo. Con esto, tenemos nuestra primera etiqueta de vértice para nuestro modelo de grafos: clientes.
Considera la segunda forma de encontrar esta información: primero podrías encontrar todas las cuentas y luego quedarte sólo con las que son propiedad de Miguel. En este caso, empiezas con los datos sobre las cuentas. Ahora tenemos una segunda etiqueta de vértice para nuestro modelo de grafo: cuentas.
Esto nos prepara para el siguiente consejo sobre cómo encontrar los perímetros en tus datos.
Regla nº 2
Si necesitas los datos para conectar conceptos, haz que esos datos sean un perímetro.
Para la consulta con la que estamos trabajando, sabemos que Miguel será una etiqueta de vértice y que su cuenta es otra etiqueta de vértice. Queda el concepto de propiedad, y sí, lo has adivinado: será el perímetro. El concepto de propiedad vincula a un cliente con una cuenta para nuestros datos de ejemplo.
Para encontrar los perímetros de tu modelo, examina tus datos. Encontrarás tus perímetros a partir de la información que vincula los conceptos entre sí y a la que tienes acceso.
Cuando trabajas con datos de grafos, estos perímetros son la pieza más importante de tu modelo de grafos. Las aristas son la razón por la que necesitas la tecnología de grafos.
Juntando estas dos, puedes derivar la siguiente regla para los modelos de grafos de propiedades etiquetados.
Regla de oro nº 3
Vértice-Borde-Vértice debe leerse como una frase u oración de tus consultas.
Nuestro consejo aquí es que escribas cómo quieres consultar con tus datos en frases cortas como "cliente posee cuenta". Identificar estas consultas y frases sigue siendo una forma sencilla de identificar cómo quieres mapear tus datos en objetos de grafos en una base de datos de grafos de propiedades, como se muestra en la Figura 4-1.
En general, las formas escritas de tus consultas sobre grafos traducirán los verbos a perímetros y los sustantivos a vértices.
Nota
No es la primera vez que la comunidad de grafos trabaja con frases semánticas y datos de grafos. Los que pertenezcáis a la comunidad semántica probablemente estéis gritando: "¡Ya hemos visto esto antes!". Y tenéis razón; lo hemos visto.1
Juntando las recomendaciones nº 2 y nº 3 se obtiene una forma específica de traducir tu forma de pensar en objetos gráficos.
Regla de oro nº 4
Los nombres y conceptos deben ser etiquetas de vértice. Los verbos deben ser etiquetas de perímetro.
Dependiendo de cómo pienses, hay momentos en los que los consejos nº 3 y nº 4 pueden crear situaciones ambiguas. Queremos ahondar aquí en algunos aspectos semánticos para ayudarte a navegar por las distintas formas en que la gente ve y piensa los datos.
En concreto, si piensas "Miguel es propietario de una cuenta", entonces "es propietario" debe ser una etiqueta de perímetro. Este es un caso en el que estás pensando activamente en la relación entre Miguel y su cuenta. Y esta línea de pensamiento activo se traduce owns
en un verbo que conecta dos datos entre sí. Así es como llegamos a "posee" como etiqueta de perímetro.
Sin embargo, hay casos en los que puedes ver este mismo escenario de forma diferente. Es decir, si estás pensando "Tenemos que representar el concepto de propiedad entre Miguel y su cuenta", entonces la propiedad debe ser una etiqueta de vértice. En ese caso, estás pensando en la propiedad como un sustantivo, es decir, una entidad. La diferencia es que, en este caso, es probable que la propiedad tenga que ser identificable. Es probable que intentes relacionar la propiedad de otras formas. En estos casos, otras preguntas que puedes plantearte son: "¿Quién estableció esa propiedad?" o "¿A quién se transfiere la propiedad si fallece el agente principal?".
Reconocemos que nos estamos metiendo en la maleza. Pero sabemos que tú también acabarás metiéndote en la maleza. Esperamos que la orientación que te proporcionamos te ayude a encontrar el camino de vuelta y a salir.
Nuestros cuatro primeros consejos introducen los fundamentos para identificar vértices y perímetros en los datos de tu gráfico. Veamos cómo razonar sobre la dirección de las etiquetas de tus perímetros.
¿Ya te has perdido? Deja que te guiemos
Las preguntas y consultas de este capítulo integran más datos en nuestro modelo. Concretamente, queremos añadir transacciones a nuestros datos para poder responder a preguntas como:
What are the most recent 20 transactions involving Michael's account?
Para responder a esta pregunta, tenemos que añadir transacciones a nuestro modelo de datos. Y estas transacciones tienen que darnos una forma de modelar y razonar sobre cómo las transacciones retiran y depositan dinero entre las cuentas, los préstamos y las tarjetas de crédito.
Cuando empiezas a escribir consultas de grafos y a iterar sobre modelos de datos, es muy fácil que te des la vuelta en tu modelo de datos. La dirección de una etiqueta de perímetro es algo difícil de razonar, por eso hacemos la siguiente recomendación.
Regla de oro nº 5
En el desarrollo, deja que la dirección de tus perímetros refleje cómo pensarías sobre los datos de tu dominio.
El consejo nº 5 infiere la dirección de una etiqueta de perímetro a medida que combinas y aplicas los consejos de los cuatro consejos anteriores. Llegados a este punto, el patrón Vértice-Borde-Vértice debería leerse fácilmente como oraciones sujeto-verbo-objeto.
Por tanto, la dirección de la etiqueta del perímetro viene del sujeto y va al objeto.
Idear etiquetas de perímetro entre transacciones es un debate que hemos visto muchas veces. Sigamos nuestro proceso de pensamiento para detallar cómo razonamos sobre el modelado de algo como una transacción en un gráfico.
Una evolución del modelado de transacciones en un grafo
Piensa en cómo añadirías primero las transacciones a tu modelo gráfico. Probablemente estés pensando en cómo una cuenta realiza transacciones con otras cuentas, o algo parecido a lo que mostramos en la Figura 4-2.
El modelo de la Figura 4-2 no funciona para nuestro ejemplo porque utiliza la idea de transacción como verbo, mientras que nuestras preguntas utilizan las transacciones como sustantivos. Queremos saber cosas como las transacciones más recientes de una cuenta y qué transacciones son pagos de préstamos. Desde este punto de vista, en realidad estamos pensando en las transacciones como sustantivos.
Por tanto, en nuestro ejemplo las operaciones deben ser etiquetas de vértices.
Ahora tenemos que razonar sobre la dirección de los perímetros. La mayoría de la gente empieza modelando la dirección de los perímetros para seguir el flujo del dinero, como se muestra en la Figura 4-3.
El reto de un modelo como el de la Figura 4 -3 es encontrar nombres intuitivos para los perímetros que faciliten la respuesta a las preguntas de nuestro capítulo. La dirección del perímetro de la Figura 4 -3 modela el flujo de dinero y es incómoda para la forma en que utilizamos las transacciones en nuestras preguntas. ¿Diríamos: "Se ha retirado dinero de esta cuenta mediante esta transacción"? Esperemos que no.
Así que la Figura 4-3 tampoco va a funcionar para nuestro ejemplo.
Recordemos las preguntas de nuestro capítulo y razonemos sobre cómo utilizamos las transacciones en las consultas. Se nos ocurrieron las siguientes frases sujeto-verbo-objeto para el contexto en el que utilizamos las transacciones en nuestro ejemplo:
-
Las transacciones se retiran de las cuentas.
-
Depósito de transacciones en cuentas.
Estas dos frases podrían funcionar; veamos cómo funcionaría con datos. En nuestros datos, podríamos modelar una transacción y cómo interactuaba con las cuentas, como se muestra en la Figura 4-4.
Para el ejemplo de este capítulo, creemos que la Figura 4-4 facilita razonablemente el uso de nuestro modelo para responder a nuestras preguntas. Esto nos da una dirección para nuestras dos etiquetas: las etiquetas de perímetro fluirán desde un Transaction
e irán a un Account
. El esquema se muestra en la Figura 4-5.
Al descomponer tus consultas en frases cortas y activas de la estructura sujeto-verbo-objeto, podrás encontrar de forma natural lo que debe ser una etiqueta de vértice o perímetro en tu modelo de grafo. Entonces la dirección de la etiqueta del perímetro vendrá del sujeto y se dirigirá al objeto.
Alejémonos de los matices del modelado de la dirección de las transacciones y volvamos al último elemento principal del esquema de un grafo: las propiedades.
¿Cuándo utilizamos las propiedades?
Repitamos la primera consulta que utilizará los vértices de transacción:
What are the most recent 20 transactions involving Michael's account?
La versión abreviada de nuestra consulta anterior se traduce en las siguientes frases cortas:
-
Michael es dueño de la cuenta
-
Transacciones retiradas de su cuenta
-
Selecciona las 20 transacciones más recientes
Hasta ahora, podemos recorrer clientes, cuentas y transacciones dentro de nuestro gráfico. Ahora nuestra pregunta pide las 20 transacciones más recientes de una cuenta. Esto significa que tenemos que subseleccionar nuestras transacciones para incluir sólo las más recientes.
Por lo tanto, querremos tener la posibilidad de filtrar las transacciones por tiempo. Esto nos lleva a nuestro último consejo relacionado con las decisiones de modelado de datos.
Regla nº 6
Si necesitas utilizar datos para subseleccionar un grupo, conviértelos en una propiedad.
Ordenar las transacciones por tiempo requiere que tengamos ese valor almacenado en nuestro modelo de grafos: introduce propiedades. Esto es un gran uso de una propiedad en el vértice de la transacción, de modo que podamos subseleccionar esos vértices en nuestro modelo. La Figura 4-6 muestra cómo añadiríamos el tiempo a nuestro ejemplo en curso.
Juntos, los consejos nº 1-6 te proporcionan un buen punto de partida para identificar lo que será un vértice, un perímetro o una propiedad en tu modelo de datos de grafos. Tenemos una última sección de buenas prácticas de modelado de datos que considerar antes de empezar con los detalles de implementación de este capítulo.
Un gráfico no tiene nombre: Errores comunes al nombrar
Los avisos de la sección siguiente son errores comunes. Cada error va seguido de nuestras recomendaciones malo-mejor-mejor.
Llegar a un consenso sobre cómo debe nombrarse y mantenerse algo con tu base de código es sorprendentemente difícil. Tenemos tres temas en los que los equipos suelen perder su valioso tiempo en buscar cómo abordar las convenciones de nomenclatura en su modelo de datos de grafos.
Errores en las convenciones de nomenclatura nº 1
Utiliza la palabra has
como etiqueta de perímetro.
Uno de los errores más comunes que vemos consiste en nombrar todos tus perímetros con la etiqueta has
, como se muestra en la parte izquierda de la Figura 4-7. Se trata de un error de denominación, porque la palabra has
no proporciona un contexto significativo sobre el propósito o la dirección del perímetro.
Si tu modelo de grafo utiliza has
para sus etiquetas de perímetro, tenemos dos recomendaciones para ti. Una etiqueta de perímetro mejor tendría la forma has_{vertex_label}
, como se muestra en el centro en naranja en la Figura 4-7. Este tipo de nombre te permite tener más especificidad en tus consultas de grafos, al tiempo que proporciona un nombre más significativo para mantener en tu base de código.
La solución preferida a este problema se muestra en verde en el extremo derecho de la Figura 4-7. Esta recomendación te aconseja utilizar un verbo activo que comunique significado, dirección y especificidad a tus datos. Vamos a utilizar las etiquetas de perímetro deposit_to
y withdraw_from
para conectar las transacciones con las cuentas en nuestros ejemplos.
Una vez seleccionadas las etiquetas de perímetro significativas, también es un error común crear nombres de propiedades que no ayuden a identificar de forma única tus datos. Esto nos lleva a nuestro siguiente escollo en el modelado de grafos de propiedades.
Errores en las convenciones de nomenclatura nº 2
Utilizando la palabra id
como propiedad.
El concepto de qué datos identifican de forma única a una entidad es un tema profundo. Utilizar una clave de propiedad llamada id
es una mala decisión porque no es descriptiva de a qué se refiere. Además, id
choca con las convenciones internas de nomenclatura de Apache Cassandra y no es compatible con DataStax Graph.
Una convención algo mejor sería nombrar la propiedad que identifica tus datos de forma única con {vertex_label}_id
, como se muestra en el centro de la Figura 4-8. Utilizamos esto unas cuantas veces a lo largo del libro porque estamos trabajando con ejemplos sintéticos, y este tipo de identificador está perfectamente bien si utilizas identificadores generados aleatoriamente, como los UUID (identificadores universalmente únicos). Sin embargo, verás que pasamos a utilizar identificadores más descriptivos cuando trabajamos con datos de código abierto. Estos identificadores representan conceptos que identifican de forma única a entidades dentro de su dominio, como números de la seguridad social, claves públicas e identificadores universalmente únicos específicos de un dominio.
Esto nos lleva al último error, y sin duda el más importante, que vemos en todo el código de las aplicaciones.
Errores en las convenciones de nomenclatura nº 3
Uso incoherente de la carcasa.
En lo que respecta a las mayúsculas y minúsculas, lo mejor es seguir las convenciones del lenguaje en el que estés escribiendo. Algunos lenguajes tienen guías de estilo que promueven CamelCase
, mientras que otros prefieren snake_case
. Para los ejemplos de este libro, tenemos previsto seguir las siguientes casillas y estilos:
-
Mayúsculas
CamelCase
para las etiquetas de los vértices -
Minúsculas
snake_case
para etiquetas de perímetro, claves de propiedades y datos de ejemplo
Este último consejo parece un poco pedante incluso para mencionarlo en un libro de grafos. Lo mencionamos porque la coherencia en las convenciones de nomenclatura tiende a olvidarse, creando costosos obstáculos para los equipos durante el último tramo de la puesta en producción de su tecnología de grafos. Cuanto más triviales le parezcan estos consejos a tu equipo, mejor será que te asegures de recordarlos.
Nuestro modelo gráfico de desarrollo completo
En el apartado anterior de sobre el modelado de datos de grafos se ilustraba cómo desglosamos nuestra primera consulta para hacer evolucionar el ejemplo del Capítulo 3. En esta sección, queremos construir los elementos restantes de nuestro modelo de datos para responder a todas las preguntas del ejemplo de este capítulo.
El ejemplo de este capítulo añade esquemas y datos que permiten a nuestra aplicación responder a las tres preguntas siguientes:
-
¿Cuáles son las 20 transacciones más recientes de la cuenta de Miguel?
-
En diciembre, ¿en qué vendedores compró Michael y con qué frecuencia?
-
Encuentra y actualiza las transacciones que más valoran Jamie y Aaliyah: los pagos de su cuenta a su préstamo hipotecario.
Ya hemos explicado cómo modelar la primera pregunta. Veámosla más detenidamente.
El esquema de grafos de la Figura 4-9 aplica los principios que construimos para responder a la primera pregunta en un modelo de datos de grafos. La nueva etiqueta de vértice es Transaction
, con dos nuevas etiquetas de perímetro al vértice Account
: withdraw_from
y deposit_to
, respectivamente. Hemos discutido cómo y dónde modelar el tiempo en nuestro grafo, que puedes ver en la Figura 4 -9 con timestamp
en el vértice Transaction
.
A continuación, vamos a considerar las preguntas restantes de este capítulo para nuestro ejemplo de este capítulo modelando las consultas:
1. In December, at which vendors did Michael shop, and with what frequency? 2. Find and update the transactions that Jamie and Aaliyah most value: their payments from their account to their mortgage loan.
Para llegar a un modelo de datos para estas preguntas, apliquemos los procesos de pensamiento que introdujimos en "Modelado de datos gráficos 101". Siguiendo los consejos que allí se daban, llegamos a tres afirmaciones sobre las transacciones:
-
Las transacciones se cargan a las tarjetas de crédito.
-
Las transacciones pagan a los proveedores.
-
Las transacciones pagan los préstamos.
A partir de estas declaraciones, podemos encontrar el resto de nuestros elementos de esquema necesarios. En primer lugar, necesitamos una nueva etiqueta de vértice para representar dónde compran nuestros clientes: Vendor
. A continuación, necesitamos una etiqueta de perímetro, pay
, para una transacción a las etiquetas de vértice Loan
o Vendor
. Por último, necesitamos otra etiqueta de perímetro, charge
, para indicar que una transacción cobra de una tarjeta de crédito.
Uniendo todo esto, tenemos el esquema que se muestra en la Figura 4-10.
Antes de empezar a construir
Hemos reducido la perspectiva completa de sobre el modelado de datos de grafos para incluir sólo las prácticas que necesitamos para nuestro ejemplo actual. Más allá de estos principios básicos, encontrarás casos de perímetro sobre tus datos que no se tratan aquí. Eso es de esperar. Estamos enseñando un proceso de pensamiento y hemos seleccionado aquí los principios como guía de partida para modelar tus datos como un gráfico.
Nota
Si pudiéramos asegurarnos de que entiendes un concepto sobre el modelado de datos en forma de gráfico, sería el siguiente: modelar tus datos como un gráfico es tanto un arte como ingeniería. El arte del proceso de modelado de datos implica crear y evolucionar tu perspectiva sobre tus datos. Esta evolución traduce tu mentalidad en el paradigma del modelado de datos que da prioridad a las relaciones.
Cuando encuentres nuevos casos de modelado en este libro o en tu propio trabajo, hazte las siguientes preguntas sobre lo que estás modelando para ayudarte a desarrollar tu propio razonamiento:
-
¿Qué significa este concepto para el usuario final de la aplicación?
-
¿Cómo vas a leer estos datos en tu aplicación?
Definir tu modelo de datos es el primer paso para aplicar el pensamiento gráfico a tu aplicación. Céntrate en los datos que puedes integrar, las consultas que quieres hacer y lo que esto significará para tu usuario final. Combinados, esos tres conceptos articulan cómo vemos, modelamos y utilizamos los datos de grafos dentro de una aplicación.
Nuestras reflexiones sobre la importancia de los datos, las consultas y el usuario final
Para ayudarte a aprender y aplicar nuestra perspectiva a la construcción de tu propio modelo gráfico, vamos a repasar la importancia de los datos, las consultas y el usuario final.
Nuestro primer consejo es que te centres en los datos que tienes. Es fácil hervir el océano modelando todo el problema gráfico de tu sector; ¡evita esta madriguera de conejo! Tu modelo de grafos evolucionará si te mantienes centrado en llegar a producción con los datos con los que trabajará tu aplicación.
En segundo lugar, aplica la práctica del diseño basado en consultas. Construye tu modelo de datos para acomodar sólo un conjunto predefinido de consultas de grafos. Una pista falsa habitual con la que nos encontramos en este tema son las aplicaciones que pretenden crear recorridos abiertos por todos los datos descubribles de un grafo. Para fines de desarrollo, la capacidad de explorar y descubrir tiene sentido. Sin embargo, para su uso en producción, una aplicación con acceso transversal abierto puede introducir una miríada de problemas.
Por implicaciones de seguridad, rendimiento y mantenimiento, aconsejamos encarecidamente a los equipos que no creen plataformas de producción con recorridos ilimitados y sin límites. La señal de advertencia que vemos es la falta de especificidad de tu aplicación de gráficos. Sabemos que esta perspectiva es muy difícil de aplicar cuando estás explorando por primera vez los datos de gráficos. Vemos la línea aquí como el establecimiento de expectativas entre lo que quieres hacer durante el desarrollo frente a lo que quieres pasar a producción en una aplicación de producción distribuida.
Por último, y lo más importante, tienes que tener en cuenta lo que significan los datos para tu usuario final. Todo, desde la selección de las convenciones de nomenclatura hasta los objetos de tu gráfico, será interpretado por otra persona: los miembros de tu equipo o los usuarios de tu aplicación. Las convenciones de nomenclatura y los objetos del gráfico son interpretados y mantenidos por los miembros de tu equipo de ingeniería; elígelos sabiamente.
En última instancia, tus datos gráficos se presentarán a un usuario final a través de tu aplicación. Dedica tiempo a diseñar tu arquitectura de datos, modelos y consultas para presentar la información que sea más significativa para ellos.
Combinados, estos tres conceptos articulan cómo vemos, modelamos y utilizamos los datos gráficos dentro de una aplicación. De nuevo, los tres conceptos son construir con los datos que tienes, seguir un diseño basado en consultas y diseñar para tu usuario final. Seguir estos principios de diseño te ayudará a desatascarte durante esas difíciles discusiones sobre el modelado de datos y a preparar tu aplicación para que sea el mejor uso de los datos de gráficos que el sector haya visto jamás.
Detalles de aplicación de la exploración de barrios en desarrollo
Nuestro esquema de la Figura 4-10 sólo requiere dos nuevas etiquetas de vértice: Transaction
y Vendor
. Lo que has practicado unas cuantas veces hasta ahora es cómo tomar el dibujo de un esquema y traducirlo a código. Te mostramos el esquema en la Figura 4-10, y en el Ejemplo 4-1 te mostramos el código.
Ejemplo 4-1.
schema
.
vertexLabel
(
"Transaction"
).
ifNotExists
().
partitionBy
(
"transaction_id"
,
Int
).
property
(
"transaction_type"
,
Text
).
property
(
"timestamp"
,
Text
).
create
();
schema
.
vertexLabel
(
"Vendor"
).
ifNotExists
().
partitionBy
(
"vendor_id"
,
Int
).
property
(
"vendor_name"
,
Text
).
create
();
Consejo
Por si te lo estás preguntando, estamos utilizando Text
como tipo de datos para la marca de tiempo, para facilitar la enseñanza de conceptos en nuestros próximos ejemplos. Utilizaremos el formato estándar ISO 8601 almacenado como texto.
Además de estas etiquetas de vértice, añadimos relaciones entre el vértice Transaction
y las demás etiquetas de vértice de este gráfico. Empecemos con las nuevas etiquetas de perímetro entre las etiquetas de vértice Transaction
y Account
. El código del esquema para las nuevas etiquetas de perímetro se muestra en el Ejemplo 4-2.
Ejemplo 4-2.
schema
.
edgeLabel
(
"withdraw_from"
).
ifNotExists
().
from
(
"Transaction"
).
to
(
"Account"
).
create
();
schema
.
edgeLabel
(
"deposit_to"
).
ifNotExists
().
from
(
"Transaction"
).
to
(
"Account"
).
create
();
Estos dos perímetros modelan cómo se mueve el dinero hacia y desde una cuenta dentro de tu banco. En el Ejemplo 4-3, añadimos el resto de etiquetas de perímetro de nuestro ejemplo:
Ejemplo 4-3.
schema
.
edgeLabel
(
"pay"
).
ifNotExists
().
from
(
"Transaction"
).
to
(
"Loan"
).
create
();
schema
.
edgeLabel
(
"charge"
).
ifNotExists
().
from
(
"Transaction"
).
to
(
"CreditCard"
).
create
();
schema
.
edgeLabel
(
"pay"
).
ifNotExists
().
from
(
"Transaction"
).
to
(
"Vendor"
).
create
();
Estas tres últimas etiquetas de perímetro completan los perímetros que necesitaremos para describir las transacciones entre los activos de nuestro ejemplo.
Generar más datos para nuestro ejemplo ampliado
A medida que crecen los ejemplos, también lo hacen los datos. Escribimos un pequeño generador de datos para ampliar los datos del Capítulo 3 e incluir nuestro modelo de datos de la Figura 4-10. Si te interesa el proceso de generación de datos de este capítulo, tienes dos opciones.
Tu primera opción es utilizar los scripts bash para recargar exactamente los mismos datos que verás en los próximos ejemplos. Te enseñaremos esta herramienta y este proceso en el Capítulo 5, pero puedes previsualizar el script de carga en el repositorio de GitHub. Te recomendamos que utilices los scripts a lo largo de este libro si quieres que los ejemplos que ejecutes localmente coincidan con los resultados que mostramos en el texto.
Tu segunda opción es sumergirte en nuestro código de generación de datos y ejecutarlo. Proporcionamos nuestro código en un cuaderno de Studio independiente llamado Ch4_DataGeneration
. Te recomendamos esta opción si quieres profundizar en la creación de datos falsos con Gremlin y en los métodos que utilizamos.
Una advertencia importante sobre el proceso de generación de datos
Si vuelves a ejecutar el proceso de inserción de datos en tu Studio Notebook, los resultados de tu gráfico local no coincidirán exactamente con los resultados impresos en este texto. Si quieres que los datos coincidan exactamente, te recomendamos importar exactamente la misma estructura de gráfico mediante DataStax Bulk Loader. Encontrarás todo esto en los materiales técnicos adjuntos.
Hasta este punto, hemos realizado muchas tareas. Hemos explorado nuestro primer conjunto de consejos de modelado de datos, hemos creado un modelo de desarrollo, hemos mirado el código del esquema y hemos insertado datos.
La última tarea principal consiste en utilizar el lenguaje de consulta Gremlin para recorrer nuestro modelo y responder a preguntas sobre nuestros datos.
Navegación básica en Gremlin
El objetivo principal de este capítulo es ilustrar un esquema de grafos del mundo real que recorre múltiples vecindarios de datos de grafos.
Consejo
Para que te sirva de referencia, a lo largo de este libro utilizaremos indistintamente las palabras caminar, navegar y recorrer para referirnos a que estamos escribiendo consultas de grafos.
Todo lo dicho en este capítulo hasta ahora ha sido necesario para preparar la respuesta a las tres preguntas siguientes en esta sección:
-
¿Cuáles son las 20 transacciones más recientes de la cuenta de Miguel?
-
En diciembre, ¿en qué vendedores compró Michael y con qué frecuencia?
-
Encuentra y actualiza las transacciones que más valoran Jamie y Aaliyah: los pagos de su cuenta a su préstamo hipotecario.
Vamos a recorrer las consultas y sus resultados. Luego, en la sección final del capítulo sobre Gremlin Avanzado, profundizaremos un poco más en cómo dar forma a la carga útil de los resultados.
Nuestra recomendación es que encuentres una forma de hacer referencia a la Figura 4-10 mientras practicas las consultas de las próximas secciones. Recomendamos hacer esto porque tu esquema funciona como tu mapa; necesitas saber dónde estás para poder caminar en la dirección correcta hacia tu destino.
Consulta 1: ¿Cuáles son las 20 transacciones más recientes de la cuenta de Miguel?
Empecemos con un pseudocódigo en el Ejemplo 4-4 para pensar cómo vamos a recorrer nuestros datos para responder a esta primera pregunta.
Ejemplo 4-4.
Question:
What
are
the
most
recent
20
transactions
involving
Michael
's account?
Process:
Start at Michael'
s
customer
vertex
Walk
to
his
account
Walk
to
all
transactions
Sort
them
by
time
,
descending
Return
the
top
20
transaction
ids
Utilizamos el proceso descrito en el Ejemplo 4-4 para crear la consulta Gremlin del Ejemplo 4-5.
Ejemplo 4-5.
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
// the customer
2
out
(
"owns"
).
// walk to his account
3
in
(
"withdraw_from"
,
"deposit_to"
).
// walk to all transactions
4
order
().
// sort the vertices
5
by
(
"timestamp"
,
desc
).
// by their timestamp, descending
6
limit
(
20
).
// filter to only the 20 most recent
7
values
(
"transaction_id"
)
// return the transaction_ids
Una muestra de los resultados:
"184"
,
"244"
,
"268"
,
...
Profundicemos en esta consulta paso a paso.
En la línea 1, dev.V().has("Customer", "customer_id", "customer_0")
busca un vértice según su identificador único. A continuación, en la línea 2, el paso out("owns")
recorre el perímetro saliente owns
hasta los vértices Account
de este cliente. En este caso, Miguel sólo tiene una cuenta.
En este punto, queremos acceder a todas las transacciones. En la línea 3, el paso in("withdraw_from", "deposit_to")
hace precisamente eso: recorremos las etiquetas de perímetro entrantes para acceder a las transacciones. En la línea 4, estamos en los vértices de las transacciones.
Nota
Dejamos un detalle fuera de "Una evolución del modelado de transacciones en un grafo" que queremos traer a colación ahora. La simplicidad de la línea 3 del Ejemplo 4-5 también fue parte de la motivación que nos llevó a diseñar los perímetros de nuestro modelo de datos. Esta primera consulta era mucho más difícil de escribir y razonar cuando los perímetros iban en direcciones distintas.
El paso order()
de la línea 4 indica que debemos proporcionar algún tipo de orden a los vértices, que son operaciones. Especificamos el orden de clasificación en la línea 5 con el paso by("timestamp", desc)
. Esto significa que vamos a acceder, combinar y ordenar todos los vértices de Transaction
según su marca de tiempo. A continuación, queremos seleccionar sólo los 20 vértices más recientes con limit(20)
. Por último, en la línea 7, queremos acceder a los transaction_ids
, así que los seleccionamos mediante el paso values("transaction_id")
.
Esta consulta devolverá una lista de valores que contiene el transaction_id
de cada una de las 20 transacciones más recientes de todas las cuentas del cliente.
Imagina cuánto más potente sería mostrar esto al usuario final. Podrían ver los detalles que son más relevantes para ellos en lugar de navegar por múltiples pantallas para unir estos datos en su cabeza. Este tipo de consulta es vital para comprender cómo personalizar tu aplicación en función de lo que más le importa a un cliente.
Pregunta 2: En diciembre de 2020, ¿en qué vendedores compró Miguel y con qué frecuencia?
Para esta segunda pregunta, empecemos con un esquema de la consulta del Ejemplo 4-6 para pensar cómo vamos a recorrer nuestros datos para responder a la pregunta.
Ejemplo 4-6.
Question:
In
December
2020
,
at
which
vendors
did
Michael
shop
,
and
with
what
frequency
?
Process:
Start
at
Michael
'
s
customer
vertex
Walk
to
his
credit
card
Walk
to
all
transactions
Only
consider
transactions
in
December
2020
Walk
to
the
vendors
for
those
transactions
Group
and
count
them
by
their
name
Iniciamos el proceso descrito en el Ejemplo 4-6 en el Ejemplo 4-7 y lo completamos en el Ejemplo 4-8. Para preparar esta consulta, utilizamos la estandarización de marcas de tiempo ISO 8601 en nuestros datos para facilitar el rango de fechas. En la norma ISO 8601, las marcas de tiempo suelen formatearse como YYYY-MM-DD’T’hh:mm:ss’Z’
, donde 2020-12-01T00:00:00Z
representa el comienzo de diciembre de 2020.
Ejemplo 4-7.
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
// the customer
2
out
(
"uses"
).
// Walk to his credit card
3
in
(
"charge"
).
// Walk to all transactions
4
has
(
"timestamp"
,
// Only consider transactions
5
between
(
"2020-12-01T00:00:00Z"
,
// in December 2020
6
"2021-01-01T00:00:00Z"
)).
7
out
(
"pay"
).
// Walk to the vendors
8
groupCount
().
// group and count them
9
by
(
"vendor_name"
)
// by their name
Los resultados son:
{
"Nike"
:
"2"
,
"Amazon"
:
"1"
,
"Target"
:
"3"
}
Advertencia
La aleatorización afecta a los resultados de la consulta 2. Si utilizas el proceso de generación de datos en lugar de cargar los datos, tu gráfico puede tener una estructura ligeramente diferente y, por tanto, recuentos diferentes para la consulta 2.
La configuración del Ejemplo 4-7 sigue un patrón de acceso similar al anterior, en el que empezamos en un cliente y luego recorremos hasta un vértice vecino. Empezamos en customer_0
y recorremos hasta sus tarjetas de crédito y luego hasta las transacciones. En las líneas 4 a 6, estamos utilizando una forma de filtrar los datos durante un recorrido. Aquí, estamos filtrando todos los vértices según sus marcas de tiempo en un rango específico. En concreto, has("timestamp", between("2020-12-01T00:00:00Z", "2021-01-01T00:00:00Z"))
ordena y devuelve todas las transacciones que tienen una marca de tiempo durante el mes de diciembre del año 2020.
En la línea 7, siguiendo nuestro esquema, vamos a los vendedores con el paso out("pay")
. Por último, queremos devolver el nombre del vendedor junto con el número de veces que se observó una transacción con ese vendedor. Lo hacemos en las líneas 8 y 9 con groupCount().by("vendor_name")
.
Además de between
, la Tabla 4-1 enumera los predicados más populares que puedes utilizar para establecer rangos sobre valores. Consulta el libro de Kelvin Lawrence para ver la tabla completa de predicados.2
Predicado | Utilización |
---|---|
eq |
Igual a |
neq |
No es igual a |
gt |
Mayor que |
gte |
Mayor o igual que |
lt |
Menos de |
lte |
Inferior o igual a |
entre |
Entre dos valores excluyendo el límite superior |
Quizá te preguntes: ¿y si quisiéramos ordenar la salida del Ejemplo 4-7?
Si quisieras devolver los resultados en orden decreciente, lo harías añadiendo el patrón order().by()
, que se muestra en en las líneas 10 y 11 del Ejemplo 4-8.
Ejemplo 4-8.
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
out
(
"uses"
).
3
in
(
"charge"
).
4
has
(
"timestamp"
,
5
between
(
"2020-12-01T00:00:00Z"
,
6
"2021-01-01T00:00:00Z"
)).
7
out
(
"pay"
).
8
groupCount
().
9
by
(
"vendor_name"
).
10
order
(
local
).
// Order the map object
11
by
(
values
,
desc
)
// according to the groupCount map's values
Los resultados ya están:
{
"Target"
:
"3"
,
"Nike"
:
"2"
,
"Amazon"
:
"1"
}
Hemos introducido el uso del ámbito en una travesía en la línea 10 con el paso order(local)
.
- Alcance
-
El alcance determina si la operación concreta debe realizarse sobre el objeto actual (local) en ese paso o sobre todo el flujo de objetos hasta ese paso (global).
Para una explicación visual del alcance en una travesía, considera la Figura 4-11.
Para explicarlo de forma sencilla, al final de la línea 9, necesitábamos ordenar el objeto del recorrido, que es un mapa. El uso de local
en la línea 10 indica a la travesía que clasifique y ordene los elementos dentro del objeto mapa. Otra forma de verlo es que queremos ordenar las entradas dentro del mapa. Lo hacemos indicando que el ámbito es local al propio objeto.
La mejor forma de entender el alcance de la travesía es jugar con diferentes consultas en tu Studio Notebook y ver cómo afecta el alcance a la forma de tus resultados. En las páginas de documentación de DataStax Graph encontrarás más diagramas visuales fantásticos para comprender el flujo de datos y los tipos de objetos.
Consejo
Si alguna vez te preguntas qué tipo de objeto tienes en medio del desarrollo de una travesía Gremlin, añade .next().getClass()
al punto en el que te encuentras en el desarrollo de tu travesía. Esto inspeccionará los objetos en este punto de tu travesía y te dará su clase.
Consulta 3: Encuentra y actualiza las transacciones que más valoran Jamie y Aaliyah: los pagos de su cuenta a su préstamo hipotecario.
La ventaja de utilizar una base de datos gráfica empieza a notarse realmente cuando recorremos múltiples vecindarios de datos, como haremos con esta tercera y última consulta. Aquí estamos accediendo y mutando datos en cinco vecindarios de datos de nuestro grafo. Vamos a dividir esta consulta en tres pasos: acceso, mutación y validación.
La primera simplificación que vamos a hacer en nuestra cuenta es reducir el alcance de la consulta. Sabemos que Jamie y Aaliyah sólo comparten una cuenta : acct_0
. Por tanto, para simplificar aún más nuestra consulta, podemos centrarnos en los paseos de una sola persona; elegimos a Aaliyah.
Esto nos lleva a la primera consulta más corta que queremos construir:
Consulta 3a: Encuentra las transacciones de Aaliyah que son pagos de préstamos
Antes de poder actualizar las transacciones importantes, tenemos que encontrar las importantes. Las transacciones que buscamos son las que indican un pago de préstamo de la cuenta conjunta de Aaliyah a la hipoteca de Jamie y Aaliyah. Esbocemos nuestro enfoque en pseudocódigo en el Ejemplo 4-9 para pensar cómo vamos a recorrer nuestros datos para responder a la pregunta.
Ejemplo 4-9.
Question:
Find
Aaliyah
's transactions that are loan payments
Process:
Start at Aaliyah'
s
customer
vertex
Walk
to
her
account
Walk
to
transactions
that
are
withdrawals
from
the
account
Go
to
the
loan
vertices
Group
and
count
the
loan
vertices
Utilizamos el proceso descrito en el Ejemplo 4-9 para crear la consulta Gremlin del Ejemplo 4-10.
Ejemplo 4-10.
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_4"
).
// accessing Aaliyah's vertex
2
out
(
"owns"
).
// walking to the account
3
in
(
"withdraw_from"
).
// only consider withdraws
4
out
(
"pay"
).
// walking out to loans or vendors
5
hasLabel
(
"Loan"
).
// limiting to only loan vertices
6
groupCount
().
// groupCount the loan vertices
7
by
(
"loan_id"
)
// by their loan_id
Los resultados de los datos de la muestra tendrán el aspecto siguiente:
{
"loan80"
:
"24"
,
"loan18"
:
"24"
}
Veamos el Ejemplo 4-10. En la línea 1, empezamos accediendo al cliente y recorremos hasta su cuenta. En la línea 2, recorremos hasta la cuenta de Aaliyah. Recordando el esquema, recorremos el perímetro de entrada withdraw_from
para acceder a las retiradas de cuenta en la línea 3.
En la línea 4, recorremos la etiqueta de perímetro pay
para llegar a los vértices Loan
o Vendor
. El paso hasLabel("Loan")
de la línea 5 es un filtro que elimina todos los vértices en este punto que no sean préstamos. Esto significa que ahora sólo tenemos en cuenta los activos en los que se ha realizado un pago desde la cuenta y que son préstamos. En la línea 6, agrupamos y contamos esos vértices de préstamos según su identificador único, como se indica en la línea 7.
La carga útil del resultado indica que esta cuenta ha realizado 24 pagos en cada préstamo del sistema.
A continuación, queremos ir un paso más allá y actualizar los datos de esta travesía para indicar qué transacciones son pagos de hipotecas.
Consulta 3b: Encuentra y actualiza las transacciones que más valoran Jamie y Aaliyah: los pagos de su cuenta corriente a su hipoteca, loan_18
El recorrido necesario para realizar esta consulta es un recorrido mutante. Todo lo que queremos decir con travesía mutante es que actualiza los datos del grafo como parte de la travesía. El Ejemplo 4-11 muestra cómo podemos utilizar el recorrido anterior para escribir propiedades en las transacciones que salen de la cuenta y entran en loan_18
, porque loan_18
es el préstamo hipotecario de Jamie y Aaliyah.
Ejemplo 4-11.
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_4"
).
// accessing Aaliyah's vertex
2
out
(
"owns"
).
// walking to the account
3
in
(
"withdraw_from"
).
// only consider withdraws
4
filter
(
5
out
(
"pay"
).
// walking to loans or vendors
6
has
(
"Loan"
,
"loan_id"
,
"loan_18"
)).
// only keep loan_18
7
property
(
"transaction_type"
,
// mutating step: set the "transaction_type"
8
"mortgage_payment"
).
// to "mortgage_payment"
9
values
(
"transaction_id"
,
"transaction_type"
)
// return transaction & type
Los resultados son:
"144"
,
"mortgage_payment"
,
"153"
,
"mortgage_payment"
,
"132"
,
"mortgage_payment"
,
...
El ejemplo 4-11 comienza igual que la primera parte de nuestra consulta. La nueva parte de este recorrido abarca las líneas 4 a 6 con los pasos filter(out("pay").has("Loan", "loan_id", "loan_18"))
. Aquí, sólo permitimos que continúen por la tubería las transacciones que están conectadas al vértice loan_18
. Esto se debe a que loan_18
es el préstamo hipotecario de Jamie y Aaliyah. En la línea 7, mutamos los vértices de transacción cambiando "tipo_transacción" por "pago_hipoteca". Al final de este recorrido, en la línea 9, queremos devolver el transaction_id
junto con su nueva propiedad, su transaction_type
.
Consulta 3c: Comprueba que no hemos actualizado todas las transacciones
Llegados a este punto, es muy útil asegurarse de que no hemos actualizado todas las transacciones de Aaliyah con mortgage_payment
. Podemos hacerlo con una comprobación rápida, que se muestra en el Ejemplo 4-12.
Ejemplo 4-12.
// check that we didn't update every transaction
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_4"
).
// at the customer vertex
2
out
(
"owns"
).
// at the account vertex
3
in
(
"withdraw_from"
).
// at all withdrawals
4
groupCount
().
// group and count the vertices
5
by
(
"transaction_type"
)
// according to their transaction_type
A continuación se muestran los resultados del Cuaderno de Studio. Fijamos unknown
como valor por defecto durante el proceso de carga de datos que también se muestra en el Cuaderno de Studio:
{
"mortgage_payment"
:
"24"
,
"unknown"
:
"47"
}
Esta consulta realiza una comprobación rápida para validar que hemos mutado correctamente nuestros datos. Combinando las líneas 1 a 3, procesamos todas las transacciones de la cuenta bancaria de Aaliyah. En la línea 4, hacemos un groupCount()
para todos esos vértices según el valor almacenado en la propiedad transaction_type
. Aquí, vemos que sólo actualizamos correctamente las 24 transacciones que son pagos de hipotecas a loan_18
. Esto valida que nuestra consulta de mutación actualizó correctamente la estructura de nuestro grafo.
Esta sección comenzó con tres preguntas, y los tres últimos ejemplos las respondieron utilizando el lenguaje de consulta Gremlin.
Hemos recorrido las consultas básicas para mostrarte por dónde empezar. Resuelve tus recorridos gráficos básicos antes de empezar a explorar toda la flexibilidad y expresividad del lenguaje de consultas Gremlin. Siempre recomendamos iterar a través de los pasos de Gremlin en modo de desarrollo para encontrar los paseos básicos que cumplan tus consultas. Esto significa que te pedimos que ejecutes la línea 1 de una consulta Gremlin y observes los resultados. Luego ejecuta las líneas 1 y 2 y mira los resultados, y así sucesivamente.
Después de haber trazado tus recorridos básicos, puedes probar Gremlin más avanzados. En este punto del desarrollo, es muy habitual encontrar formas de crear estructuras de carga útiles específicas para pasarlas a tu punto final.
Cubriremos las estrategias más populares para construir JSON con Gremlin en la siguiente sección.
Gremlin avanzado: Dar forma a tus resultados de consulta
El objetivo de esta sección es construir una versión más avanzada de nuestra consulta Gremlin que responda a una nueva pregunta de :
Is there anyone else who shares accounts, loans, or credit cards with Michael?
Nos gustaría introducir una nueva pregunta para demostrar conceptos avanzados de Gremlin dentro de un pequeño vecindario de datos. Una vez que entiendas cómo se aplican estos conceptos a esta pregunta, te invitamos a que utilices el cuaderno que acompaña a este capítulo para poner en práctica los conceptos de las demás consultas introducidas en "Navegación básica con Gremlin".
Trabajaremos para dar forma a los resultados de nuestra nueva consulta en varias etapas. Éstas son
-
Dar forma a los resultados de la consulta con los pasos
project()
,fold()
, yunfold()
-
Eliminar datos de los resultados con el patrón
where(neq())
-
Planificación de cargas útiles de resultados robustos con el paso
coalesce()
Consejo
Para quien se sumerja más a fondo en el mundo de las consultas Gremlin, recomendamos encarecidamente el detalle y las explicaciones del libro Practical Gremlin: An Apache TinkerPop Tutorial, de Kelvin Lawrence.3
Dar forma a los resultados de la consulta con los pasos proyectar(), plegar() y desplegar()
Cuando empezamos a escribir una nueva consulta, nos gusta ir construyendo poco a poco las piezas necesarias. Uno de los pasos Gremlin más útiles es el paso project()
, porque nos ayuda a construir un mapa específico de datos a partir de nuestra consulta. Empecemos a construir nuestra consulta definiendo las tres claves que queremos tener en nuestro mapa: CreditCardUsers
, AccountOwners
, y LoanOwners
.
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
constant
(
"name or no owner for credit cards"
)).
4
by
(
constant
(
"name or no owner for accounts"
)).
5
by
(
constant
(
"name or no owner for loans"
))
Esta estructura de consulta es la base de lo que estamos construyendo. En este ejemplo, queremos empezar con una persona concreta: Miguel. Luego queremos crear una estructura de datos que tendrá tres claves: CreditCardUsers
, AccountOwners
, y LoanOwners
. Creamos este mapa con el paso project()
de la línea 2. Los argumentos del paso project()
son las tres claves. Para cada clave del paso project()
, queremos tener un paso by()
. Cada modulador by()
crea los valores asociados a las claves:
-
El modulador
by()
de la línea 3 creará un valor para la teclaCreditCardUsers
. -
El modulador
by()
de la línea 4 creará un valor para la claveAccountOwners
. -
El modulador
by()
de la línea 5 creará un valor para la teclaLoanOwners
.
Echemos un vistazo a los resultados en este punto:
{
"CreditCardUsers"
:
"name or no owner for credit cards"
,
"AccountOwners"
:
"name or no owner for accounts"
,
"LoanOwners"
:
"name or no owner for loans"
}
Esta es una buena línea de base para trabajar. A continuación, vamos a recorrer la estructura de nuestro gráfico para empezar a rellenar los valores de nuestro mapa. Empezaremos con los datos de la primera clave: encontrar a las personas que comparten una tarjeta de crédito con Miguel.
Volviendo a nuestro esquema, tendremos que recorrer el perímetro uses
para llegar a las tarjetas de crédito. Después volveremos a pasar por el perímetro uses
para volver a las personas. Después, queremos acceder a sus nombres. En Gremlin, añadiríamos este recorrido en las líneas 3, 4 y 5:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
values
(
"name"
)).
6
by
(
constant
(
"name or no owner for accounts"
)).
7
by
(
constant
(
"name or no owner for loans"
))
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
values
(
"name"
)).
6
by
(
constant
(
"name or no owner for accounts"
)).
7
by
(
constant
(
"name or no owner for loans"
))
Los únicos pasos que añadimos fueron caminar desde Michael hasta su tarjeta de crédito a través del perímetro uses
en la línea 3. Luego, en la línea 4, caminamos de vuelta a todas las personas que utilizan esa tarjeta de crédito. La carga útil resultante es
{
"CreditCardUsers"
:
"Michael"
,
"AccountOwners"
:
"name or no owner for accounts"
,
"LoanOwners"
:
"name or no owner for loans"
}
Esto confirma lo que sabemos: Michael no compartió ninguna tarjeta de crédito con otras personas. Esperábamos ver su nombre en el conjunto de resultados.
Ahora hagamos lo mismo con la siguiente clave de nuestro mapa: AccountOwners
. Aquí, queremos recorrer el perímetro owns
hasta el vértice de la cuenta y volver al vértice de la persona:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
values
(
"name"
)).
6
by
(
out
(
"owns"
).
7
in
(
"owns"
).
8
values
(
"name"
)).
9
by
(
constant
(
"name or no owner for loans"
))
Veamos la carga útil resultante:
{
"CreditCardUsers"
:
"Michael"
,
"AccountOwners"
:
"Michael"
,
"LoanOwners"
:
"name or no owner for loans"
}
Observando estos datos, no vemos lo que cabría esperar. Esperábamos ver a María como valor resultante de AccountOwners
. María no aparece porque Gremlin es perezoso; devuelve el primer resultado, no todos los resultados. Tenemos que añadir una barrera para forzar que todos los resultados terminen y vuelvan.
La barrera que nos gusta utilizar aquí es fold()
. El paso fold()
esperará a que se encuentren todos los datos y luego enrollará los resultados en una lista. Esto es una ventaja, porque ahora podemos crear reglas de tipos de datos específicas para nuestra aplicación. La consulta ajustada es la siguiente
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
values
(
"name"
).
6
fold
()).
7
by
(
out
(
"owns"
).
8
in
(
"owns"
).
9
values
(
"name"
).
10
fold
()).
11
by
(
constant
(
"name or no owner for loans"
))
La forma de los datos en la carga útil resultante es la que esperábamos ver:
{
"CreditCardUsers"
:
[
"Michael"
],
"AccountOwners"
:
[
"Michael"
,
"Maria"
],
"LoanOwners"
:
"name or no owner for loans"
}
Completemos la construcción de nuestro mapa añadiendo las declaraciones en el último paso de by()
. Estas sentencias tienen que ir desde Miguel hasta su préstamo y luego volver. La consulta y el conjunto de resultados son:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
values
(
"name"
).
6
fold
()).
7
by
(
out
(
"owns"
).
8
in
(
"owns"
).
9
values
(
"name"
).
10
fold
()).
11
by
(
out
(
"owes"
).
12
in
(
"owes"
).
13
values
(
"name"
).
14
fold
())
{
"CreditCardUsers"
:
[
"Michael"
],
"AccountOwners"
:
[
"Michael"
,
"Maria"
],
"LoanOwners"
:
[
"Michael"
]
}
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
values
(
"name"
).
6
fold
()).
7
by
(
out
(
"owns"
).
8
in
(
"owns"
).
9
values
(
"name"
).
10
fold
()).
11
by
(
out
(
"owes"
).
12
in
(
"owes"
).
13
values
(
"name"
).
14
fold
())
{
"CreditCardUsers"
:
[
"Michael"
],
"AccountOwners"
:
[
"Michael"
,
"Maria"
],
"LoanOwners"
:
[
"Michael"
]
}
En este punto, tenemos los resultados esperados. Vemos que Miguel comparte una cuenta con María. Y vemos que Miguel no comparte tarjetas de crédito ni préstamos con nadie más.
Para algunas aplicaciones, no es útil devolver que Michael comparte una tarjeta de crédito consigo mismo. Veamos cómo eliminaríamos a Miguel de esta carga útil resultante.
Eliminar datos de los resultados con el patrón where(neq())
Puede que te resulte útil eliminar a Miguel del conjunto de resultados. Podemos hacerlo utilizando el paso as()
para almacenar el vértice de Miguel, y luego eliminarlo del conjunto de resultados. Puedes eliminar un vértice de tu canalización con el paso where(neq
("some_stored_value"))
.
La siguiente versión de nuestra consulta, en la que hemos aplicado directamente este paso a cada sección, se muestra en el Ejemplo 4-13.
Ejemplo 4-13.
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
as
(
"michael"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
where
(
neq
(
"michael"
)).
6
values
(
"name"
).
7
fold
()).
8
by
(
out
(
"owns"
).
9
in
(
"owns"
).
10
where
(
neq
(
"michael"
)).
11
values
(
"name"
).
12
fold
()).
13
by
(
out
(
"owes"
).
14
in
(
"owes"
).
15
where
(
neq
(
"michael"
)).
16
values
(
"name"
).
17
fold
())
A continuación se muestran los resultados completos del Ejemplo 4-13:
{
"CreditCardUsers"
:
[],
"AccountOwners"
:
[
"Maria"
],
"LoanOwners"
:
[]
}
Las principales adiciones a nuestra consulta se producen en las líneas 1, 5, 10 y 15 de la consulta anterior. En la línea 1, almacenamos el vértice de Miguel con el paso as("michael")
. Echemos un vistazo a lo que ocurre con where(neq("michael"))
en la línea 5, que es lo mismo que ocurre en las líneas 10 y 15.
Para entender lo que ocurre en la línea 5, debes recordar en qué parte del gráfico te encuentras. Al final de la línea 4, estamos en Customer
vértices. Concretamente, estamos procesando clientes que comparten una cuenta con Michael
. Aquí es donde entra en juego el paso where(neq("michael"))
. Queremos aplicar un filtro verdadero/falso a cada vértice de la cadena. La prueba del filtro verdadero/falso es si ese vértice es o no igual a Miguel: where(neq("michael"))
. Si el vértice es Miguel, la línea 5 lo elimina del recorrido. Si el vértice no es Miguel, pasa el filtro y permanece en la cadena.
Planificación de cargas útiles de resultados robustas con el paso coalesce()
Dependiendo de las reglas de estructura de datos de tu equipo, puede que no sea preferible comprobar si un valor de la carga útil de datos es una lista vacía. Podemos ayudarte a diseñarlo.
Podemos implementar la lógica try/catch para que tu consulta no devuelva una lista vacía. Lo haremos para la primera clave del mapa: CreditCardUsers
. Después de hacerlo, añadiremos todos los detalles de la consulta para los dos pasos restantes de by()
.
Rebobinemos y volvamos a la construcción de la carga útil JSON para el valor asociado a CreditCardUsers
. Partimos de aquí:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
as
(
"michael"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
where
(
neq
(
"michael"
)).
6
values
(
"name"
).
7
fold
()).
8
by
(
constant
(
"name or no owner for accounts"
)).
9
by
(
constant
(
"name or no owner for loans"
))
{
"CreditCardUsers"
:
[],
"AccountOwners"
:
"name or no owner for accounts"
,
"LoanOwners"
:
"name or no owner for loans"
}
Puedes implementar la lógica try/catch en Gremlin con el paso coalesce()
. Queremos dar forma a los resultados de modo que siempre haya un valor en las listas para cada clave, como "CreditCardUsers": ["NoOtherUsers"]
. Empecemos por ver cómo integrar el paso coalesce
en nuestra consulta:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
as
(
"michael"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
where
(
neq
(
"michael"
)).
6
values
(
"name"
).
7
fold
().
8
coalesce
(
constant
(
"tryBlockLogic"
),
// try block
9
constant
(
"catchBlockLogic"
))).
// catch block
10
by
(
constant
(
"name or no owner for accounts"
)).
11
by
(
constant
(
"name or no owner for loans"
))
La carga útil resultante es:
{
"CreditCardUsers"
:
"tryBlockLogic"
,
"AccountOwners"
:
"name or no owner for accounts"
,
"LoanOwners"
:
"name or no owner for loans"
}
Cuando utilizas el paso coalesce()
en la línea 8, toma dos argumentos. El primer argumento está en la línea 8 y puede considerarse como la lógica del bloque try. El segundo argumento está en la línea 9 y puede considerarse como la lógica del bloque catch.
Si la lógica del bloque de prueba tiene éxito, los datos resultantes se pasan a la cadena. En este caso, con fines ilustrativos, utilizamos algo que definitivamente tendría éxito: el paso constant()
. Este paso devolvió la cadena "tryBlockLogic"
que vemos en la carga útil resultante. El paso constant()
es útil por muchas razones, una de las cuales es que puede servir como marcador de posición mientras construyes consultas más complicadas. Así es como lo estamos utilizando aquí.
Si el primer argumento del paso coalesce()
falla en la línea 8, el segundo argumento se ejecutará en la línea 9. Veamos cómo podemos utilizar esto para rellenar lo que queremos en nuestra carga de datos:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
as
(
"michael"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
where
(
neq
(
"michael"
)).
6
values
(
"name"
).
7
fold
().
8
coalesce
(
unfold
(),
// try block
9
constant
(
"NoOtherUsers"
))).
// catch block
10
by
(
constant
(
"name or no owner for accounts"
)).
11
by
(
constant
(
"name or no owner for loans"
))
{
"CreditCardUsers"
:
"NoOtherUsers"
,
"AccountOwners"
:
"name or no owner for accounts"
,
"LoanOwners"
:
"name or no owner for loans"
}
En la línea 8, la lógica que hemos añadido al bloque try es unfold()
. Esto está intentando tomar los resultados del paso anterior y desplegarlos con éxito. Los resultados en este punto de la cadena son una lista vacía []
. En Gremlin, no puedes desplegar un objeto vacío. Esto lanza una excepción que es capturada por el bloque try. Por lo tanto, ejecutamos la línea 9, el segundo argumento del paso coalesce()
: constant("NoOtherUsers")
. Por eso vemos la entrada "CreditCardUsers": "NoOtherUsers"
en nuestro resultado.
Lamentablemente, hemos perdido nuestra estructura de lista garantizada. Podemos volver a añadirla con un fold()
después del paso coalesce()
:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
as
(
"michael"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
where
(
neq
(
"michael"
)).
6
values
(
"name"
).
7
fold
().
8
coalesce
(
unfold
(),
9
constant
(
"NoOtherUsers"
)).
fold
()).
10
by
(
constant
(
"name or no owner for accounts"
)).
11
by
(
constant
(
"name or no owner for loans"
))
{
"CreditCardUsers"
:
[
"NoOtherUsers"
],
"AccountOwners"
:
"name or no owner for accounts"
,
"LoanOwners"
:
"name or no owner for loans"
}
Los pasos que hemos añadido de la línea 5 a la 9 crean una estructura de datos predecible para intercambiar en toda tu aplicación. Será JSON bien formateado sobre el que otras aplicaciones podrán razonar.
A continuación, tenemos que añadir esta lógica try/catch a cada paso de by()
. El patrón lógico completo que hay que añadir al final de cada paso de by()
en nuestra consulta completa es:
coalesce
(
unfold
(),
// try to unfold the names
constant
(
"NoOtherUsers"
)).
// inject this string if there are no names
fold
()
// structure the results into a list
Este patrón Gremlin garantiza que tengamos una lista no vacía en la carga útil resultante. La consulta completa y sus resultados son:
1
dev
.
V
().
has
(
"Customer"
,
"customer_id"
,
"customer_0"
).
as
(
"michael"
).
2
project
(
"CreditCardUsers"
,
"AccountOwners"
,
"LoanOwners"
).
3
by
(
out
(
"uses"
).
4
in
(
"uses"
).
5
where
(
neq
(
"michael"
)).
6
values
(
"name"
).
7
fold
().
8
coalesce
(
unfold
(),
9
constant
(
"NoOtherUsers"
)).
fold
()).
10
by
(
out
(
"owns"
).
11
in
(
"owns"
).
12
where
(
neq
(
"michael"
)).
13
values
(
"name"
).
14
fold
().
15
coalesce
(
unfold
(),
16
constant
(
"NoOtherUsers"
)).
fold
()).
17
by
(
out
(
"owes"
).
18
in
(
"owes"
).
19
where
(
neq
(
"michael"
)).
20
values
(
"name"
).
21
fold
().
22
coalesce
(
unfold
(),
23
constant
(
"NoOtherUsers"
)).
fold
())
{
"CreditCardUsers"
:
[
"NoOtherUsers"
],
"AccountOwners"
:
[
"Maria"
],
"LoanOwners"
:
[
"NoOtherUsers"
]
}
Creemos que la construcción iterativa y el paso a paso de Gremlin es la mejor manera de entender el lenguaje de consulta. Este libro trata de enseñarte nuestros procesos de pensamiento, y así es como pensamos al utilizar Gremlin. Hay más de una forma de escribir una consulta gráfica; esperamos que sientas curiosidad por utilizar otros pasos para procesar los mismos datos. Descubrirlo puede ser tan fácil como abrir un Studio Notebook y explorar nuevos pasos por tu cuenta.
Pasar del desarrollo a la producción
Retomando nuestra analogía con el submarinismo del principio de este capítulo, nuestro tiempo de entrenamiento en la piscina ha llegado a su fin. Tal y como lo vemos, la progresión a través de los ejemplos técnicos de este capítulo es igual que el aprendizaje del control de la flotabilidad o la resolución de problemas en aguas profundas dentro de una piscina. En algún momento, habrás aprendido todo lo posible practicando en un entorno controlado.
Con los cimientos que hemos construido en los últimos capítulos, ha llegado el momento de dar el salto fuera de tu entorno de desarrollo y construir una base de datos gráfica lista para la producción.
Antes de que te preocupes demasiado, esto no significa que debas saber todo lo que hay que saber sobre los datos gráficos. Todavía hay innumerables temas que seguimos explorando nosotros mismos.
Lo que sí significa, sin embargo, es que creemos que estás preparado para adentrarte en una comprensión más profunda del uso de datos de grafos en sistemas distribuidos. Hemos creado este ejemplo para prepararte para un último paso en la capa de datos físicos de la comprensión de las estructuras de datos de grafos en Apache Cassandra. En concreto, el próximo capítulo te mostrará cómo optimizar tus estructuras de grafos para aplicaciones distribuidas.
Al ilustrar cómo pensamos a través de los datos de los gráficos, hemos colocado a propósito algunas trampas en el ejemplo de este capítulo. En el próximo capítulo, te mostraremos estas trampas y te guiaremos a través de su resolución. Este próximo capítulo será el último que utilice nuestro ejemplo C360, ya que describirá la iteración final en la creación de un esquema gráfico de calidad de producción para este ejemplo.
1 Ora Lassila y Ralph R. Swick, "Resource Description Framework (RDF) Model and Syntax Specification", 1999. https://oreil.ly/zWcnO
2 Kelvin Lawrence, Practical Gremlin: An Apache TinkerPop Tutorial, 6 de enero de 2020, https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html.
3 Kelvin Lawrence, Practical Gremlin: An Apache TinkerPop Tutorial, 6 de enero de 2020, https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html.
Get Guía del profesional de los datos gráficos now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.