Convierte 6000 notas en un cerebro conectado

Vistas: 1
0:00 / 0:00
Convierte 6000 notas en un cerebro conectado

Llevo desde 2008 escribiendo en atareao.es, y con los años he acumulado más de 6000 notas en Markdown. Toda una vida de apuntes, tutoriales y pruebas de todo tipo. Estaba convencido de que ahí dentro estaba todo lo que he aprendido, hasta que un día busqué particiones y no apareció la nota que habla de redimensionar un volumen LVM. Busqué monitorización de red y me salieron las notas de ntopng y de iftop, que no usan esa palabra por ningún lado. Y busqué alta disponibilidad sabiendo que tenía tres notas relacionadas, y me devolvió una. El conocimiento estaba ahí, el problema era que yo no sabía que estaba ahí. 😅

Y eso es exactamente lo que te quiero contar hoy. En el episodio 837 te hablé de la teoría del GraphRAG y de los tres límites del RAG clásico. En este episodio vamos un paso más allá y lo llevamos a lo material, construimos el grafo. Te voy a explicar cómo se trocea una nota, cómo se extraen entidades con un modelo local, cómo se conecta todo y cómo se ve ese grafo desde arriba. Nada de recorrer código, conceptos primero y ejemplos después.

Qué es GraphRAG y por qué el RAG clásico no llega

Antes de meterme en faena necesito que tengas claro por qué existe esta cosa. Porque si no entiendes el problema, GraphRAG suena a palabra de moda y a marketing.

El RAG clásico, el de toda la vida, lo que hace es trocear tus documentos en fragmentos, calcular un embedding de cada fragmento y guardar todos esos vectores. Cuando tú preguntas algo, convierte tu pregunta en un vector, busca los fragmentos más parecidos y se los da al modelo como contexto. Para preguntas locales funciona de maravilla. Si le preguntas cómo configuro Nginx como proxy inverso, te va a recuperar el fragmento correcto y te va a responder bien.

El problema aparece con dos cosas. La primera es que el RAG clásico no sabe que las cosas están relacionadas. Para él, Docker, Podman, Compose y Kubernetes son cuatro fragmentos sueltos que casualmente comparten vecindario vectorial. No sabe que son una familia, que todos hablan de lo mismo, de empaquetar y ejecutar aplicaciones. Y la segunda es que no tiene visión global. No hay manera de preguntarle de qué van todas mis notas y que te dé una respuesta que abarque el conjunto, porque solo sabe enseñarte fragmentos sueltos.

Ahí es donde entra el GraphRAG, que es una idea de Microsoft Research, publicada en 2024 en el paper From Local to Global, a Graph RAG Approach to Query-Focused Summarization. La propuesta es construir, además del índice vectorial, un grafo de entidades y relaciones usando un modelo de lenguaje. Y encima de ese grafo, pregenerar resúmenes de las comunidades de entidades que aparecen juntas. Cuando tú preguntas, además de recuperar fragmentos parecidos, puedes recuperar el vecindario de un nodo en el grafo y el resumen de la comunidad a la que pertenece.

Fíjate en la diferencia, porque es la clave de todo el artículo.

RAG vectorialGraphRAG
Qué indexaFragmentos y embeddingsFragmentos, embeddings y un grafo de entidades
Qué recuperaLos fragmentos más parecidosFragmentos, vecindario del grafo y resúmenes de comunidad
Responde bien aPreguntas localesPreguntas locales y también globales
Punto débilNo sabe que Docker y Kubernetes son familiaEl coste de indexar, que se paga una sola vez

En mi caso particular no estoy hablando de teoría. Llevo meses construyendo herramientas alrededor de esto, y hoy lo que te traigo ya está funcionando, con todas mis notas y sin que se salga un solo byte de mi equipo.

El pipeline, en cinco fases

Lo bonito de todo esto es que, por debajo, es un proceso encadenado en cinco fases, y cada fase alimenta a la siguiente. El orden no es arbitrario.

Primero se escanea la carpeta donde tienes las notas y se localizan todos los ficheros .md. Segundo se trocea cada nota por secciones, que es lo que llamamos chunking. Tercero se hace el NER, que es extraer las entidades de cada fragmento con el modelo. Cuarto se conecta todo creando las aristas entre los nodos. Y quinto se guarda el resultado en la base de datos.

Cinco fases y, como verás un poco más adelante, un solo comando. La herramienta que utilizo es graphrag, un proyecto en Rust que publiqué como graphrag-search en crates.io. Un único binario, sin Python, sin npm, sin servidores. Toda la inteligencia artificial pasa por Ollama, que corre en tu propia máquina. Para instalarlo,

cargo install graphrag-search

Y necesitas Ollama en marcha con dos modelos, uno pequeño para los embeddings (bge-m3) y otro para el NER y los resúmenes (llama3.2:3b). Para que no te pelees con las descargas, te he dejado un script de preparación que comprueba que Ollama responde, baja los modelos si faltan y localiza el binario. Lo tienes en este gist, gist.atareao.es/atareao/0c3f13ca721b40c0997c7abbba62f205, y lo ejecutas con dos órdenes.

chmod +x setup.sh
./setup.sh

Con eso tienes el terreno preparado. Ahora vamos con la primera fase de verdad, que es la que más gente hace mal.

Chunking, trocear por estructura y no por caracteres

Lo primero que hay que entender es que una nota grande no se puede tratar como un bloque único. Y no lo digo solo por el límite de tokens del modelo, aunque también. Lo digo por algo más profundo.

Si yo le paso a la IA un documento de dos mil líneas que habla de qué es un proxy inverso, de cómo instalar Nginx, de certificados SSL con Certbot y de balanceo de carga, lo que obtengo no es la suma de esas cuatro ideas. Obtengo una media de todo. El significado se difumina, se convierte en una sopa donde ni Nginx ni Certbot destacan. Es como preguntarle a alguien por su vida entera y que te responda que fue variada. 😅 No sirve.

Así que troceamos. Pero con criterio, y esto es lo importante. No partimos por número de caracteres, porque eso rompe bloques de código, corta listas por la mitad y le da al modelo fragmentos que no significan nada. Troceamos por estructura, aprovechando la estructura que ya tiene tu nota. Y hay cuatro reglas.

La primera, cada título de nivel uno, dos o tres, es decir, #, ## y ###, abre un fragmento nuevo. Cada sección es una idea, y cada idea es la unidad mínima de búsqueda.

La segunda, el frontmatter, el bloque YAML de arriba entre los tres guiones, no es contenido, son metadatos. Se separa y se aparta. Ese frontmatter es el que luego nos permitirá tener el título y las etiquetas de la nota, y no se mete dentro de ningún fragmento.

La tercera, y esta es sagrada, nunca jamás se rompe un bloque de código. Si una sección tiene un bloque con sus tres acentos graves y dentro hay una configuración de Nginx de treinta líneas, ese bloque viaja entero. Partirlo sería mutilarlo. Media configuración de Nginx no sirve absolutamente para nada.

Y la cuarta, los fragmentos de menos de cincuenta caracteres se descartan. Un título suelto, una línea vacía, un ver también sin nada detrás. Eso es ruido, y fuera.

Vamos con el caso real, que es lo que aclara. Coge una nota mía de ejemplo, nginx-proxy-inverso.md, con un frontmatter que declara título, fecha y etiquetas.

---
title: Nginx como proxy inverso
date: 2025-02-10
tags: nginx, redes, linux, ssl
---

El frontmatter no acaba dentro de ningún fragmento, se procesa aparte. El encabezado de nivel uno, que es el título de la nota, tampoco genera fragmento, es el encabezado y ya está. Y quedan tres secciones, y por tanto tres fragmentos. En la base de datos, esa nota es la que tiene el identificador 96 y sus fragmentos son estos.

fragmentoencabezadotamaño
24## Qué es un proxy inverso566
25## Instalación y configuración básica813
26## SSL con Nginx y Certbot621

Y lo puedes comprobar tú mismo con una consulta a la base de datos.

sqlite3 grafo.db "SELECT id, header FROM chunks WHERE note_id = 96;"

Fíjate en el detalle del fragmento de instalación. Dentro viajan dos bloques de código completos, el de apt install nginx y el de la configuración del servidor, y ninguno se corta por la mitad. El troceador reconoce las vallas de acentos graves y trata el bloque como una unidad atómica. Esto importa mucho, tanto para que la configuración te sirva de algo como porque el NER que viene después extrae mejores entidades de un bloque íntegro.

Lo que quiero que te lleves de esta fase es una idea, cada fragmento es una idea autónoma. Es la unidad mínima de búsqueda y también la unidad sobre la que trabajamos en todo lo que viene después. Tres ideas, no una media de las tres.

NER, extraer entidades con un modelo local

Vamos con la parte que parece magia y no lo es, extraer entidades. NER son las siglas de Named Entity Recognition, reconocimiento de entidades nombradas. La idea es sencilla, convertir texto libre en una lista de conceptos.

Aquí los conceptos son concretos, herramientas, lenguajes de programación, bases de datos, librerías, frameworks, conceptos, protocolos, organizaciones, sistemas operativos, formatos. En el binario hay doce categorías de tipo, y son las que el modelo puede asignar a cada entidad que encuentra.

Ahora la pregunta interesante, ¿quién hace la extracción? La respuesta corta es un modelo de lenguaje local a través de Ollama. Y la pregunta lógica es, ¿por qué un modelo de lenguaje y no un diccionario fijo de términos? Pues precisamente por la misma razón por la que falla el RAG clásico. Un diccionario solo reconoce lo que ya está en la lista. Si mañana tu nota habla de una herramienta que acaba de salir, o de una que tú llamas de una forma particular, el diccionario se queda ciego. Un modelo entiende el contexto, sabe que en la frase configuración SSL con Nginx y Certbot hay tres cosas técnicas distintas, y sabe distinguirlas de verbos y de sustantivos genéricos.

Y el criterio es este. De cada entidad extraemos tres cosas, la etiqueta, el tipo y una confianza entre cero y uno. Por ejemplo, la etiqueta sería Nginx, el tipo sería herramienta, y la confianza sería un número. Y aquí está el detalle fino, solo conservamos las entidades que igualan o superan 0.5. ¿Por qué? Porque prefiero perder una entidad dudosa que llenar el grafo de ruido. Un grafo con ruido es un grafo en el que no confías, y un grafo en el que no confías no lo usas nunca.

Vamos al ejemplo, que es lo que aclara. Coge el fragmento de la nota anterior, el de ## SSL con Nginx y Certbot. Su texto menciona Nginx, Certbot, Let’s Encrypt, SSL y TLS. El modelo devuelve esas entidades y graphrag las guarda como nodos. Este es el resultado real.

entidadtipoconfianza
Nginxframework0.8
Certbottool0.8
SSLconcept1.0
Let’s Encryptorganization0.5
TLSconcept0.5

Fíjate en el detalle, Let’s Encrypt se clasifica como organización, no como herramienta. Y TLS y SSL como conceptos. El modelo no solo detecta el nombre, también intuye qué es. Y fíjate también en las que no aparecen, configurar, servidor, con, porque no son entidades, son simplemente palabras. El filtro de confianza y el buen juicio del modelo hacen el trabajo.

El campo de metadatos guarda la confianza y el modelo que la generó, como JSON.

{"score":0.8,"source":"llama3.2:3b"}

Una cosa que podrías hacer a mano, de hecho es lo que haces en muchos de tus artículos cuando pones etiquetas al final. Esto es lo mismo, pero de forma completamente automática y sin que tú te tengas que preocupar una a una.

Y una nota de eficiencia antes de seguir, porque aquí está una de las decisiones de diseño más importantes. En lugar de llamar al modelo fragmento a fragmento, se le manda el archivo entero en una sola llamada, con marcadores que separan cada sección. Cuando te devuelve la información, ya la tienes toda junta. Menos ida y vuelta, mismo resultado, y el proceso de construcción se acelera muchísimo. Ese es el puente entre tus notas tal y como las escribes y la estructura que necesitamos después.

Nodos y aristas, cada línea significa algo

Ya tenemos fragmentos y entidades. Ahora toca conectarlo todo, y aquí está una de las partes más elegantes de todo el diseño. Porque un grafo no son puntos y líneas sin más, cada línea significa algo.

Empecemos por los nodos, que hay tres tipos. El primero es la nota, el documento completo. El segundo es la entidad, que es Nginx, Certbot o SSL. Y el tercero es el tag, que sale de dos sitios, de las carpetas donde organizas las notas y de las etiquetas que ya escribes en el frontmatter.

Y ahora las aristas, que son cuatro, y cada una responde a una pregunta distinta. Esto es lo que quiero que interiorices, las aristas tienen semántica.

La primera es mentioned_in, que se lee aparece en. Va de la entidad a la nota y responde a la pregunta, ¿dónde se menciona Nginx? Su peso es la confianza que le dio el NER. Si Nginx salió con 0.8, la arista pesa 0.8.

La segunda es co_occurs_with, coaparece con. Va de entidad a entidad y responde a, ¿qué conceptos salen juntos en el mismo fragmento? Y aquí está el detalle bonito. El peso es el mínimo de las dos confianzas. Si Nginx salió con 0.8 y Certbot con 0.8, la arista entre ellos pesa 0.8. Y ¿por qué el mínimo y no la media? Porque si una de las dos apariciones es dudosa, la relación entera es sospechosa. Usar el mínimo evita contaminar el grafo con parejas que en realidad no lo son. Es conservador a propósito.

La tercera es belongs_to, pertenece a. Va de la nota a su carpeta, con peso uno siempre. Y la cuarta es tagged_with, etiquetada con. Va de la nota al tag, también con peso uno.

Sobre el peso quiero darte una metáfora, porque creo que ayuda. Un grafo no es una lista de amistades todas igual de estrechas, no. Hay conocidos y hay amigos íntimos. El peso mide esa cercanía, y gracias a él, cuando explores el grafo, los vecinos con conexiones fuertes contarán más que los débiles. Nginx y Certbot son amigos íntimos, Nginx y un concepto genérico son un saludo de pasillo.

Vamos con el ejemplo completo. Coge otra vez nginx-proxy-inverso.md. Tú no has escrito nada de esto, y sin embargo el grafo lo genera automáticamente.

(nota:nginx-proxy-inverso) ──belongs_to──> (node:redes)
(nota:nginx-proxy-inverso) ──tagged_with──> (node:nginx)
(nginx)   ──mentioned_in──> (nota:nginx-proxy-inverso)   peso 0.8
(certbot) ──mentioned_in──> (nota:nginx-proxy-inverso)   peso 0.8
(nginx)   ──co_occurs_with──> (certbot)                  peso 0.8

Cinco aristas, cuatro tipos, cero intervención manual. El listado es una simplificación didáctica, porque en el grafo real de esa nota hay bastantes más aristas, cuatro tags y varias menciones. Pero mira lo importante, todo esto lo ha deducido la herramienta. Tú solo pusiste las etiquetas en el frontmatter y organizaste las carpetas. El resto se ha generado solo.

Y con este comando, de un vistazo, ves el vecindario de Nginx, las notas donde aparece, las entidades con las que sale y sus tags.

graphrag graph 'nginx' graph.db -d 2

La estructura ya está ahí. Lo único que faltaba era construirla.

Construir el grafo con graphrag build

Y llegamos al momento de la verdad, construir todo esto. Y la primera noticia es buena, es una orden y listo.

graphrag build ~/notas graph.db

Al ejecutar ese comando, por debajo pasa todo el pipeline del que te hablaba antes, se escanean los ficheros, se trocean, se extraen las entidades, se conectan los nodos y las aristas y se guarda todo. Cinco pasos y una sola orden. Si quieres forzar el modelo de NER, se lo indicas explícitamente.

graphrag build ~/notas graph.db --ner-model llama3.2:3b

Pero hay dos ideas que hacen que esto merezca la pena, y ninguna es evidente a primera vista.

La primera es que el build es incremental. Cada nota guarda un hash SHA256 de su contenido. Cuando vuelves a construir, graphrag compara ese hash y solo procesa las notas nuevas o modificadas. Si no has tocado una nota, no la vuelve a mirar. Y además es limpio, si borras una nota, en el siguiente build se detecta y se poda, junto con las entidades que se quedan huérfanas. Nada de fantasmas en tu grafo.

La segunda idea es que el build es paralelo. Mientras varias notas se procesan a la vez en distintos hilos, la escritura se hace de forma ordenada, en un único hilo. Y ¿por qué? Pues porque SQLite aguanta muchas lecturas a la vez pero solo una escritura. Separando el trabajo pesado, que es la extracción de entidades y que sí va en paralelo, de la escritura serial, evitamos que los hilos se peleen por el fichero. Es la receta clásica, mucho cocinero y un solo camarero. En mi equipo levanto cuatro hilos de NER y un escritor.

Mira el log real de la primera pasada sobre un corpus de prueba de 24 notas.

[INFO  graphrag::graph::build] Notas existentes en BD: 0
[INFO  graphrag::graph::build] Encontrados 24 archivos .md en ./notas-muestra
[INFO  graphrag::graph::build] Archivos a procesar (nuevos/modificados): 24
[INFO  graphrag::graph::build] Procesando con 4 hilos en paralelo (NER) + 1 escritor
...
[INFO  graphrag::graph::build] Grafo construido: 📊 Resumen del grafo:
    ├── Archivos procesados: 24
    ├── Entidades extraídas: 138
    ├── Total nodos:        201
    └── Total aristas:      495

Ese mensaje de Entidades extraídas 138 es el bruto, con repeticiones. El NER puede sacar la misma entidad en fragmentos distintos, y el grafo la deduplica al guardar. Cuando mires las estadísticas verás que las entidades únicas son menos, pero eso lo vemos en un momento.

Ahora el contraste, que es lo que quiero que recuerdes. Vuelves a lanzar el mismo comando sin tocar nada, y pasa esto.

[INFO  graphrag::graph::build] Notas existentes en BD: 24
[INFO  graphrag::graph::build] Encontrados 24 archivos .md en ./notas-muestra
[INFO  graphrag::graph::build] Archivos a procesar (nuevos/modificados): 0
[INFO  graphrag::graph::build] Procesando con 4 hilos en paralelo (NER) + 1 escritor
[INFO  graphrag::graph::build] Grafo construido: 📊 Resumen del grafo:
    ├── Archivos procesados: 0
    ├── Entidades extraídas: 0
    ├── Total nodos:        201
    └── Total aristas:      495

Archivos a procesar, cero. No se ha reescrito nada, y la segunda pasada es casi instantánea porque no vuelve a llamar al modelo. Eso es la incrementalidad en acción.

En mi caso particular tengo más de 6000 notas, y la primera construcción, con todos esos documentos, me va a llevar tranquilamente un día entero. En los archivos del proyecto hay un orden de magnitud para que sepas a qué atenerte, con un modelo pequeño y cuatro hilos son unos 0.08 ficheros por segundo, lo que para 6254 notas se traduce en unas veintidós horas. Es mucha tela, así que si tienes un volumen grande de documentos, mi consejo es que hagas esa primera pasada con GPU o, si te da igual, con un modelo en la nube. Ahora, el día a día, cuando solo has añadido o corregido unas pocas notas, lo haces perfectamente en local. Como te decía al principio, en mi caso, cuando metí ochenta notas nuevas, el build me estimó seis minutos.

Y aquí está la gran ventaja de todo esto. Esto lo puedes dejar en un proceso que se ejecute todos los días. Todos los días mira tus notas, ve si hay cambios, los procesa y los guarda. Y ahí está el premio, todo esto sin nube, sin claves de API y sin que ninguna de tus notas salga de casa.

Si quieres afinar el comportamiento, el binario lee un fichero de configuración en ~/.config/graphrag/config.toml, con este aspecto.

db = "graph.db"
ollama_url = "http://localhost:11434"
embed_model = "bge-m3:latest"
ner_model = "llama3.2:3b"
summary_model = "llama3.2:3b"
k = 5
depth = 2
alpha = 0.7
notes_dir = ""
num_threads = 4

Ahí lo tienes todo, la dirección de Ollama, los tres modelos que usa, el número de hilos, el directorio de notas y un par de parámetros de búsqueda que tocaremos en el siguiente episodio.

Un fichero SQLite que llevas en el bolsillo

Y el remate de esta fase es que todo eso acaba en un único fichero SQLite. Portátil, lo copias a un pendrive, lo llevas a otro equipo y funciona. No hay que montar un servidor, ni una base de datos en la nube, ni nada parecido. Un fichero que cabe en cualquier sitio y que no sale de tu máquina.

Para ver la foto de tu grafo en cualquier momento, este comando.

graphrag stats grafo.db

Y te devuelve esto.

📊 Estadísticas del grafo:
├── Nodos totales:     201
├── Notas:             24
├── Entidades:         125
├── Tags:              52
├── Aristas:           495
├── Chunks:            58
└── Con embeddings:    58

Ahí tienes la cuenta que manda. 201 nodos son 24 notas más 125 entidades más 52 tags. Los 58 fragmentos tienen su embedding calculado, listos para buscar por similitud. Y fíjate que las entidades únicas son 125, no las 138 que decía el resumen del build, porque el grafo deduplica. La fuente de verdad es siempre stats.

Sobre el tamaño, para que te hagas una idea, la base de datos de mis 6000 documentos ocupa unos 500 MB. Un fichero que llevas en el bolsillo y que contiene el mapa completo de todo lo que he aprendido en quince años. 🔥

Comunidades con Leiden, el grafo visto desde arriba

Y llegamos al último bloque, que es el que responde directamente a uno de los límites que le pusimos al RAG clásico, la visión global. Porque hasta ahora todo lo que hemos hecho es muy bonito, pero es local, te dice qué hay alrededor de un nodo. Lo que no te dice es cómo está organizado tu conocimiento por encima de ese detalle.

Y ahí entran las comunidades. Una comunidad es un grupo de conceptos que están muy conectados entre ellos y poco con el resto. Piensa en tu propia base de notas, seguro que hay un racimo de cosas sobre contenedores, otro sobre redes, otro sobre bases de datos. No los has organizado así a propósito, pero están ahí. El grafo lo revela.

Esto es justo lo que me pasaba a mí con el blog. Yo veía mis artículos como cosas deslavazadas, uno detrás de otro. Pero la realidad es que he ido paso a paso buscando un objetivo concreto, y hay una guía argumental que probablemente tú no conozcas porque yo la he omitido. El grafo lo saca a la luz.

¿Y cómo se detectan las comunidades? Con un algoritmo que se llama Leiden, y no te voy a contar las matemáticas porque no te hacen falta. Quédate con la intuición, los amigos de mis amigos tienden a ser un grupo. Es la misma lógica por la que en un instituto acaban formándose pandillas sin que nadie las organice. Leiden recorre el grafo y encuentra esos racimos de forma automática.

Si te interesa el rigor, Leiden viene de un paper de 2019 de Traag, Waltman y van Eck, From Louvain to Leiden, guaranteeing well-connected communities. La gracia frente al algoritmo anterior, Louvain, es que Leiden garantiza que las comunidades resultantes están bien conectadas, mientras que Louvain puede producir hasta un veinticinco por ciento de comunidades mal conectadas y un dieciséis por ciento directamente desconectadas. Y encima Leiden es bastante más rápido en redes reales. Por eso lo elegí.

Para detectar las comunidades,

graphrag community detect graph.db --resolution 0.8

Y aquí hay un aviso práctico que quiero que te lleves grabado. La resolución es la palanca que controla el grano de la partición. Valores altos prefieren racimos pequeños y exigentes, valores bajos agrupan más y hacen comunidades más gordas. Con la resolución por defecto, que es uno, Leiden tiende a fusionar demasiado en corpus pequeños y puede no encontrar ningún corte. En mi demo, con esa resolución por defecto, el resultado fue cero comunidades. Cero. 😅 No es que el grafo esté mal, es que la resolución está mal elegida para ese tamaño.

Probando valores reales sobre la misma base, sale esta tabla.

resolucióncomunidades
0.230
0.523
0.822
1.00

Hay un acantilado entre 1.0 y 0.9. De 0.9 hacia abajo sí salen comunidades, y de ahí hacia arriba, nada. La elección razonable en este corpus es 0.8, que da veintidós comunidades sin romper el grafo en trozos diminutos. Así que si lanzas el comando con el valor por defecto y te sale cero, ya sabes, baja la resolución.

Hay otro detalle que conviene que sepas, y es un pequeño desfase del propio binario. Cuando ejecutas community detect, el CLI reporta Nivel 1, cero y Nivel 2, cero, pero las comunidades sí existen. Lo que pasa es que la detección las guarda en el nivel cero y luego el comando las cuenta mirando los niveles uno y dos, con lo que te informa de ceros. La fuente de verdad es la tabla de la base de datos.

sqlite3 grafo.db "SELECT COUNT(*) FROM communities;"

Y ahí están las veintidós. Veintidós comunidades detectadas sobre solo veinticuatro notas.

Déjame que te enseñe una de verdad, la mayor de todas, que tiene nueve miembros. Se llama community_0_34 y estos son sus integrantes.

126|miapp|person
127|miusuario|person
128|/opt/miapp|path
129|network.target|concept
130|multi-user.target|concept
131|systemctl|tool
132|daemon-reload|tool
133|enable|tool
134|on-failure|protocol

Se explica solo, ¿verdad? miapp, miusuario, /opt/miapp, systemctl, daemon-reload, enable y los targets de systemd. Todo pertenece a la nota de crear un servicio con systemd. Leiden ha visto que esas nueve entidades aparecen siempre juntas y las ha agrupado.

Detectar no basta. El paso que convierte cada comunidad en algo que un modelo de lenguaje puede usar es el resumen, y ese es el segundo comando.

graphrag community summarize graph.db

Aquí le pides a la IA que lea los miembros de cada comunidad y escriba una descripción de qué va ese racimo. Mira el log real.

📝 Resumiendo comunidades con 'llama3.2:3b'...
[INFO  graphrag::community::summarize] community 250 ('community_0_3'): summarized (283 tokens)
[INFO  graphrag::community::summarize] community 251 ('community_0_6'): summarized (348 tokens)
...
[INFO  graphrag::community::summarize] community 262 ('community_0_34'): summarized (602 tokens)
...
✅ 22 comunidades resumidas (6214 tokens totales)

Y la tarjeta de presentación que el modelo le puso a community_0_34 fue esta.

A community of entities related to system administration and configuration, specifically focusing on Linux-based systems. The community appears to be centered around the management of system services, processes, and configurations, with a focus on ensuring the stability and reliability of the system.

El resumen salió en inglés, porque el modelo pequeño a veces responde en su idioma, pero la idea es exacta, administración de servicios y configuración de Linux. Eso es precisamente lo que agrupa esa comunidad.

Y fíjate en por qué esto importa de verdad. El RAG clásico te daba los fragmentos más parecidos a tu consulta, y punto. No tenía forma de saber que Docker, Podman, Compose y Kubernetes eran una familia. Ahora sí. Ahora tienes, además de la lista de conceptos, una descripción de qué va cada racimo de tu conocimiento. Eso es contexto, no una lista. Y ese contexto es el que se inyecta en las respuestas cuando preguntas algo global, para que la respuesta tenga el marco adecuado. Es la visión global que le faltaba al RAG.

Tus notas ya eran un grafo

Vamos a recapitular, porque el viaje de hoy no ha sido corto.

Hemos empezado por el chunking, por qué no puedes tratar una nota como un bloque y cómo trocearla respetando títulos, frontmatter y bloques de código, descartando los fragmentos de menos de cincuenta caracteres. Cada fragmento, una idea autónoma.

Hemos seguido con el NER, cómo un modelo local convierte texto libre en una lista de entidades con etiqueta, tipo y confianza, conservando solo las que igualan o superan 0.5, porque es mejor perder una que llenar el grafo de ruido.

Después, los nodos y las aristas, con tres tipos de nodo y cuatro tipos de conexión, cada una con su semántica y su peso, y con el mínimo de confianza como criterio conservador en las coocurrencias.

Luego, el build, una orden, incremental por hash, paralelo con escritor serial y un único fichero SQLite portátil. La primera vez tarda, las siguientes son casi instantáneas.

Y por último, las comunidades, Leiden agrupando conceptos cercanos y resumiéndolos con IA para recuperar la visión global.

Y todo esto con una herramienta que insisto en señalar por lo que representa, un único binario Rust, publicado como graphrag-search en crates.io, con toda la inteligencia artificial pasando por Ollama en tu propia máquina. Cero Python, cero npm, cero servidores, cero datos saliendo de tu equipo.

La idea que quiero que te lleves es una sola, tus notas ya contienen un grafo, lo único que hacía falta era hacerlo visible. Fragmento a fragmento, entidad a entidad, arista a arista, y al final comunidades que te cuentan la historia completa de lo que sabes.

Yo lo estoy utilizando ya para algo muy concreto. Cuando creo un episodio nuevo, le pido al grafo los episodios y los capítulos relacionados, y así quien llega a un episodio suelto tiene un contexto mayor. Es esa visión global que yo, sin querer, he ido creando durante años, pero que ahora está ahí, a la vista.

Y me queda una pregunta rondando, porque me la estoy haciendo yo desde hace días. ¿De qué sirve tener un mapa si no sabes recorrerlo? Ya tienes el grafo construido, las entidades conectadas y las comunidades resumidas. Pero a la hora de buscar, de combinar lo semántico con lo estructural, de subir por el grafo o quedarte en la superficie, de decidir cuándo mandan los vectores y cuándo las relaciones, ahí es donde se separa el hombre del niño. Eso es exactamente lo que veremos en el siguiente episodio, la búsqueda híbrida.

Mientras tanto, la tarea para casa es sencilla. Coge tus notas, lanza un graphrag build y prueba community detect con distintas resoluciones para ver qué comunidades aparecen. Te aseguro que te vas a sorprender de lo que has escrito sin darte cuenta. 😊


Más información

Deja una respuesta