GraphRAG, tus notas por fin responden (y sin inventarse nada)

Vistas: 0
0:00 / 0:00
GraphRAG, tus notas por fin responden (y sin inventarse nada)

Hay una frase que llevo repitiendo tres semanas y que hoy por fin puedo cerrar, un grafo que no se consulta es un mueble. Y mira que me ha costado llegar hasta aquí. En el episodio anterior, haciendo unas pruebas que no venían demasiado a cuento, me ventilé de un plumazo toda la información que tenía indexada y tuve que reconstruirla desde cero con el Slimbook y su NVIDIA. En ese disgusto encontré una lección que te voy a contar hoy mismo, si dejas que el repositorio crezca hasta las seis mil notas antes de indexar, el primer build se te hace cuesta arriba, mientras que si lo haces poco a poco casi no te enteras. Así que después de entender qué es un GraphRAG (episodio 837) y de construirlo pieza a pieza (episodio 839), hoy llegamos a lo único que de verdad importa cuando llevas años escribiendo notas, interrogarlas.

De qué veníamos, la trilogía completa

Antes de entrar en harina, déjame que te sitúe, porque han sido tres episodios y conviene tener el mapa en la cabeza. En el 837 te expliqué qué es esto del GraphRAG, qué sentido tiene y para qué sirve. En el 839 te conté cómo se construye, con un binario que me he hecho yo para el ejemplo, pero que tiene primos mucho mejores y más potentes, y que solo pretendía enseñarte las posibilidades que tienes sobre la mesa. Y hoy, en el 841, toca lo que nos faltaba, interrogar al grafo.

Fíjate en la progresión, porque es la de cualquier cuaderno de notas que crece. Primero tienes notas sueltas. Luego las conectas. Y al final quieres preguntarle al conjunto. Pues ese último paso es el que hoy desmenuzamos, desde lo más simple (una búsqueda que te devuelve notas ordenadas) hasta lo más ambicioso (una pregunta en lenguaje natural que te responde citando en qué nota se apoya).

Y te adelanto el cierre, porque quiero que lo tengas presente desde ya. El vector te dice qué se parece, el grafo te dice cómo se conecta, y juntos te dicen lo que no sabías que buscabas. Esa frase es el resumen de la trilogía entera.

El vector y el grafo, dos formas de mirar tus notas

Llevamos mucho tiempo hablando de embeddings, así que conviene fijarlos antes de seguir. Un embedding no es más que una representación vectorial, un vector de n dimensiones (ojo, que no son tres) que coloca los textos de significado parecido en direcciones parecidas. Cuando buscas algo, tu consulta se convierte en otro vector y se mide el ángulo que forma con cada uno de los tuyos. Cuanto más pequeño el ángulo, más se parecen. Eso es el vector, y te dice en qué se parecen dos cosas.

El grafo, en cambio, es una red de pesca. Nodos unidos por aristas. No sabe nada de significados, pero entiende de conexiones, de qué está al lado de qué, de qué comparte una entidad con qué. Es exactamente lo que montamos en el episodio anterior escaneando los Markdown, extrayendo entidades con un LLM local y guardando todo en SQLite.

Déjame que lo aterrice con la búsqueda que más me ha ayudado a explicarlo, la de servidor web seguro. Si buscas solo con vectores, pasa una cosa muy típica. La nota que mejor encaja, Hardening de un servidor web, sube arriba con un 1.000 y no hay discusión. Pero mira las que vienen detrás. Fail2ban cae a 0.054, Seguridad en contenedores Docker a 0.013 y Certbot: certificados SSL con Let’s Encrypt se queda en 0.000, como si no existiera.

¿Y por qué se hunde la de Certbot? Pues porque su texto habla de certificados, dominios y renovación, y en ningún sitio dice servidor web seguro con esas palabras. El vector la mira, compara significados y decide que va de otra cosa. Resultado, te devuelve media respuesta y tú ni te enteras del hueco.

Si buscas solo con el grafo pasa lo contrario. Le pides lo conectado con Nginx y te devuelve todo lo que toca Nginx, la nota de proxy inverso sí, pero también rendimiento, logs, virtual hosts, balanceo. Mucho ruido. Y encima no sabe ordenar por relevancia semántica, así que le da el mismo peso a una nota que menciona Nginx de pasada que a tu artículo estrella sobre configurar un proxy inverso.

Así que ninguno de los dos gana. Los dos se necesitan, y cómo se combinan es justo lo que viene a continuación.

Las cifras de mi grafo

Antes de meterme con los comandos, te doy los números reales de mi instalación, porque ayudan a dimensionar de qué estamos hablando. Cuando consulto las estadísticas del grafo que uso a diario, me encuentro con algo más de veinte mil nodos, unas seis mil trescientas notas, quince mil entidades y, atención al dato, cuarenta y seis mil trozos con embeddings.

Ese último número es el que más cuesta asimilar. Cuarenta y seis mil pedazos salidos de seis mil trescientas notas. El binario no guarda las notas como bloques indivisibles, las parte en chunks manejables y a cada chunk le calcula su vector. De ahí que el coste de indexar no dependa solo de cuántas notas tengas, sino de cuánto texto contienen.

Y luego están las comunidades, que te resumo ahora y detallo más adelante. Una comunidad no es más que un grupo de cosas relacionadas detectado automáticamente. En mi grafo hay comunidades que giran alrededor de GIMP y del desarrollo de plugins, otras alrededor de Android, otras alrededor de restic y los backups. Cada una trae un resumen con sus rasgos característicos, y esos resúmenes son el panorama que luego usaremos para responder.

Para que te hagas una idea a escala pequeña, la demo con la que hice las pruebas está construida sobre doce notas de andar por casa, y ahí las estadísticas son de ciento cuarenta y tres nodos, cuatrocientas cincuenta y dos aristas, ciento cuarenta y cinco entidades extraídas y sesenta y cuatro chunks. La proporción se mantiene, muchas más aristas que notas, porque cada nota aporta varias entidades y cada entidad se repite por muchos sitios.

¿Te haces una idea del potencial? Yo llevo años escribiendo, y hasta hace tres semanas todo eso era un montón de ficheros que solo sabía encontrar cuando me acordaba del título exacto. Ahora es una red que puedo recorrer.

La arquitectura híbrida, tres fases y un dial

La búsqueda híbrida de graphrag no es una idea abstracta, es una tubería con tres fases por las que pasa tu consulta, y un dial que la modula. Vamos una por una.

La primera es la búsqueda vectorial. Tu pregunta se convierte en un vector y se compara por similitud de coseno contra todos los vectores guardados. Aquí todavía no entra el grafo, aquí solo se responde a la pregunta de qué se parece a lo que busco. De tus notas sale un puñado de candidatas, las K más próximas.

La segunda es el reranking. Esos candidatos se reordenan, y aquí entran un par de bonificaciones de sentido común. Si tu término aparece en el título de la nota, sube. Si el resultado es una nota y no una entidad suelta, sube. Y aquí aparece el dial, el famoso alfa, que reparte el peso entre la puntuación que viene del vector y la que viene del grafo. Alfa a 1.0 es solo vectores, alfa a 0.0 es solo grafo, y por defecto viene en 0.7, donde el significado importa y la conexión complementa.

La tercera es la expansión por grafo, y es la parte bonita. Desde los mejores candidatos, el recorrido salta a sus vecinos, dos saltos por defecto, y esos vecinos entran en los resultados con puntuación decreciente. Cuanto más lejos del candidato original, menos peso. Esto es lo que hace que asomen notas que no compartían ni una palabra con tu consulta, pero que están a uno o dos pasos de una que sí.

El modelo mental que quiero que te lleves, primero se parece, luego se ordena, después se conecta. Y en cada fase tienes una palanca. Y ojo, que la búsqueda no es una caja negra, te explica el porqué de lo que te devuelve y tú la ajustas.

Una nota honesta sobre la primera fase, search necesita siempre un modelo para embeber la consulta. En mi caso corro Ollama en local, con lo que lo tengo resuelto, y si no, echaría mano a cualquier servicio de embeddings y funcionaría igual. Pero sin modelo no hay vector, y sin vector no hay búsqueda. No hay atajo.

Aprender a leer los resultados

Uno de los objetivos de este episodio era que aprendieras a leer la salida, porque es lo que separa a quien usa esto de quien lo sufre. Así que te voy a mostrar una búsqueda real y la desmenuzamos.

Con la consulta servidor web seguro, un -k 5 y los valores por defecto, la salida viene a ser esta,

🔍 Consulta: 'servidor web seguro'
   k=5, depth=2, alpha=0.7

🧠 HYBRID SEARCH (depth=2)
──────────────────────────────────────────────────
        1.000  [note    ] Hardening de un servidor web (seguridad/hardening-servidor-web.md)  (vecinos: 20)
   └── ## Conclusión: El hardening es acumulativo: ninguna medida aislada basta, pero juntas elevan
muchísimo el coste para un atacante ...
        0.868  [note    ] "Fail2ban: protegiendo SSH y servicios" (seguridad/fail2ban.md)  (vecinos: 20)
        0.862  [note    ] Seguridad en contenedores Docker (docker/docker-seguridad.md)  (vecinos: 20)
        0.861  [note    ] "Certbot: certificados SSL con Let's Encrypt" (nginx/certbot-ssl.md)  (vecinos: 20)
        0.040  [tag     ] seguridad  (vecinos: 0)
        0.040  [tag     ] firewall  (vecinos: 0)
        0.040  [tag     ] nginx  (vecinos: 0)
        0.000  [concept ] jail  (vecinos: 0)

Fíjate en tres cosas, porque son las que vas a mirar siempre. La primera, la puntuación, 1.000, 0.868, 0.862, 0.861. Es el orden de relevancia y sube o baja según lo que ajustes. La segunda, el tipo, las notas van marcadas como note, pero debajo asoman tag y concept con puntuaciones que son migajas. La tercera, el porqué, que viene más abajo.

Ese porqué es oro. El comando te lista los vecinos del resultado ganador y las comunidades relacionadas, así ves negro sobre blanco de dónde sale cada cosa. En el bloque de vecinos de Hardening de un servidor web aparecen sus etiquetas (seguridad, firewall, nginx, servidor-web, hardening), sus notas conexas a un salto (Fail2ban, Balanceo de carga con Nginx, Kubernetes en producción, Certbot), entidades como https y http, y hasta un vecino a dos saltos, restic init. Y en el bloque de comunidades, dos resúmenes con su puntuación, uno sobre frameworks web de Python y otro sobre restic y los backups.

Ahora vuelve al detalle del que presumía hace un rato. La nota de Certbot no dice servidor web seguro en ningún sitio, y aun así aparece con 0.861. Aparece porque en el grafo está conectada con nginx y con https, aunque el vector, por sí solo, la hundiera en 0.000. Eso es exactamente lo que jamás te habría dado un grep, ni un rg, ni un vector solitario.

Las palancas, alfa, profundidad, filtros y formatos

Con el dibujo claro, las palancas son fáciles de recordar.

La primera es -k, cuántos resultados quieres. La segunda es -d, cuánto salta el grafo, con -d 0 como interruptor de seguridad que apaga la expansión y te deja el comportamiento del RAG clásico. La tercera es -a, el dial entre vectores y grafo.

Déjame que te enseñe el efecto del dial con el mismo ejemplo. Subimos alfa a 0.9, casi todo vectores, y las notas se aprietan en la parte alta, Fail2ban pasa de 0.868 a 0.894, Docker de 0.862 a 0.890 y Certbot de 0.861 a 0.888, mientras los tags caen a 0.011. Bajamos alfa a 0.5, el grafo pesa mucho más, y la cosa se invierte, las notas bajan (0.825, 0.817, 0.815) y los tags suben de 0.011 a 0.088. Lo que cambia no es el ganador, es cuánta voz tiene la conexión. Con 0.9 el vector manda, con 0.5 el grafo empieza a empujar vecinos que el vector, solo, habría despreciado.

Y luego está el experimento que a mí me dejó con la boca abierta, el de la profundidad. Misma consulta, dos valores.

Solo vectorial, sin expansión, -d 0,

🧠 HYBRID SEARCH (depth=0)
        1.000  [note    ] Hardening de un servidor web (seguridad/hardening-servidor-web.md)  (vecinos: 0)
        0.054  [note    ] "Fail2ban: protegiendo SSH y servicios" (seguridad/fail2ban.md)  (vecinos: 0)
        0.013  [note    ] Seguridad en contenedores Docker (docker/docker-seguridad.md)  (vecinos: 0)
        0.000  [note    ] "Certbot: certificados SSL con Let's Encrypt" (nginx/certbot-ssl.md)  (vecinos: 0)

Y con dos saltos de expansión, el valor por defecto, -d 2,

🧠 HYBRID SEARCH (depth=2)
        1.000  [note    ] Hardening de un servidor web (seguridad/hardening-servidor-web.md)  (vecinos: 20)
        0.868  [note    ] "Fail2ban: protegiendo SSH y servicios" (seguridad/fail2ban.md)  (vecinos: 20)
        0.862  [note    ] Seguridad en contenedores Docker (docker/docker-seguridad.md)  (vecinos: 20)
        0.861  [note    ] "Certbot: certificados SSL con Let's Encrypt" (nginx/certbot-ssl.md)  (vecinos: 20)
        0.040  [tag     ] seguridad  (vecinos: 0)

Las notas ganadoras son las mismas en los dos casos, pero los números cuentan una historia distinta. Con -d 0, el vector solo hunde a Fail2ban en 0.054 y a Certbot en 0.000, apenas los considera relevantes. Con -d 2, la expansión por grafo los rescata hasta 0.868 y 0.861, y hace asomar los tags. No aparece una nota nueva de la nada, lo que ocurre es que se recuperan los vecinos y se reescalan las notas que el vector despreciaba. Ahí está toda la diferencia entre un RAG clásico y un GraphRAG.

Aparte de esas tres palancas gruesas, están las finas. --filter te deja filtrar por los campos que tus notas exponen en el metadata JSON. Varios filtros se combinan con un AND y el campo admite rutas anidadas tipo a.b. Eso sí, el filtro solo funciona sobre campos que existan de verdad.

Y aquí te tengo que contar un pequeño fracaso, porque es lo que te vas a encontrar en la vida real. Lanzo graphrag search backups --filter 'date >= 2024' y me devuelve sin resultados, cuando las doce notas son de 2024. ¿Qué pasa? Pues que el filtro se aplica sobre el metadata del nodo, y en mi base las notas guardan content, hash, path y slug. La fecha del frontmatter no llega al metadata, así que date no casa con nada. En cambio, filtrar por ruta sí funciona,

graphrag search nginx grafo.db --filter 'path = nginx/nginx-proxy-inverso.md'

Y ahí te devuelve la nota que toca. La lección es sencilla, antes de fiarte de un filtro, comprueba con stats o con un sqlite3 rápido qué campos tienes. Moraleja de grabación, siempre es la misma, que torpeza la mía por no mirarlo antes. 😅

Siguen --min-weight, que descarta aristas débiles durante la expansión, esas co-ocurrencias accidentales que solo meten ruido en repositorios grandes, y --notes-only, que esconde entidades y tags y te deja solo notas, que es lo que quieres el noventa por ciento de las veces. Y para terminar, --format cambia la salida, table para leerla, list para que te la devuelva como enlaces Markdown y los pegues en una nota, y json para que lo consuma otro programa.

Mira el ejemplo de list con la consulta python,

graphrag search python grafo.db --notes-only --format list
- [Frameworks de Python para web](python/python-frameworks.md)
- [Health checks y monitorización](infraestructura/health-checks.md)
- [Estrategia de backups con restic](backups/backups-restic.md)

Fíjate en las dos últimas. No hablan de Python, pero están conectadas con la principal y el grafo las trae. Y si en lugar de list pides json, te devuelve un objeto por resultado con id, label, type, score, el fragmento, el fichero de origen y la lista de neighbors con su distancia. Eso es materia prima perfecta si quieres montar tu propia interfaz o encadenarlo con otra herramienta.

Responder con fuentes, el comando que lo cambia todo

Hasta aquí hemos buscado y nos han devuelto listas. Muy útiles, pero al final tú tienes que leer cinco notas y montar la respuesta en tu cabeza. El siguiente paso es que la máquina te dé la respuesta ya montada, y eso es ask.

ask junta las tres piezas de la trilogía. Primero recupera con la búsqueda híbrida que acabamos de ver. Luego contextualiza con los resúmenes de las comunidades, esos grupos que detectamos con el algoritmo de Leiden. Y por último responde con un LLM local vía Ollama, citando las fuentes.

Antes de preguntar hay que detectar y resumir las comunidades, y eso se hace una sola vez, no en cada consulta,

graphrag community detect grafo.db
graphrag community summarize grafo.db

Y ahora sí, la pregunta. Le pido cómo configurar Traefik,

graphrag ask "¿cómo configuro Traefik?" grafo.db

Y me devuelve una respuesta que empieza igual que la de cualquier manual, que Traefik es un proxy inverso, y sigue con los pasos para configurarlo. Hay un detalle curioso, la respuesta me sale en inglés. No sé muy bien por qué, supongo que es cosa del modelo que tengo configurado, pero da igual, porque lo que importa está justo debajo.

📚 Sources:
   - Instalación de Traefik
   - Primeros pasos con Traefik
   - Traefik con Docker
   - certificados con Let's Encrypt
   - ...

Y ahí está la diferencia fundamental con un chat cualquiera. Tú le preguntas a un chatbot genérico cómo se configura Traefik y te responderá bien, casi seguro. Pero te responde de su memoria, de lo que aprendió durante su entrenamiento, y puede que esté mezclando versiones o inventándose un flag que no existe. ask no inventa. Se apoya en tus notas, en tus configuraciones, en las que tú has escrito y probado, y te dice de dónde saca cada cosa.

Fíjate en los dos tipos de fuente que aparecen mezclados. Están las notas, con su título, que son la materia prima real, y luego las entidades que el grafo ha extraído y que el retrieval ha pescado por su cuenta, cabeceras HTTP, conceptos, nombres de herramientas. Que la respuesta se apoye en una nota de certificados cuando preguntas por un proxy inverso es justo la magia del grafo, esa nota no habla de proxy inverso con esas palabras, pero está conectada y ha venido como apoyo para la parte de TLS.

Hay dos ajustes que merece la pena conocer. --communities decide cuántos resúmenes de comunidad entran en el contexto, por defecto tres. Si lo subes, el modelo recibe más panorama y gasta más tokens, si lo bajas a cero, le quitas el contexto de comunidades y lo dejas solo con los fragmentos literales. Y --model te deja elegir qué modelo de Ollama redacta la respuesta, con uno más grande tendrás respuestas mejor hiladas a cambio de más espera.

Y ahora la parte que a mí me parece de buen diseño, si Ollama no está en marcha, ask no se cae. Avisa, se salta la generación y termina con código de salida cero. Lo he probado apuntando a un Ollama muerto y la salida es esta,

⚠️  Ollama no disponible: no se pudo recuperar contexto.
⚠️  answer generation skipped; is Ollama running?

Ese detalle de degradar sin estridencias es el que te permite encadenar graphrag dentro de un cron o de un script sin que la ausencia de Ollama te tumbe el proceso entero. Y hay un matiz técnico que conviene tener claro, sin Ollama tampoco hay embeddings, así que ni siquiera corre la fase vectorial y el retrieval sale vacío. Pero el programa informa y sigue, en lugar de escupir un error.

Sobre esto del grounding quiero ser honesto contigo, porque es la palabra de moda para decir que la respuesta se ancla a una fuente real. Que la respuesta cite fuentes reduce las alucinaciones, no las elimina. El propio benchmark ALCE, que mide la calidad de las citas, encontró que hasta los mejores modelos carecen de un apoyo de cita completo la mitad de las veces. Así que toma las fuentes como una red de seguridad excelente, pero sigue abriendo la nota antes de fiarte del todo. La respuesta es un atajo, no un acto de fe.

Comunidades, build incremental y un cron

Las comunidades merecen un párrafo propio, porque son la pieza que le da perspectiva al conjunto. Se detectan con el algoritmo de Leiden, que a diferencia del clásico Louvain garantiza que las comunidades estén bien conectadas, y luego se resumen con el LLM. En mi base de demo salieron dos, una alrededor de los frameworks web de Python y otra alrededor de restic y los backups. Cada resumen ocupa unos cientos de tokens y ese texto es el panorama que ask inyecta cuando la pregunta toca esos palos.

Un detalle curioso y honesto de grabación. El comando de detección me imprime Nivel 1: 0, Nivel 2: 0, como si no hubiera encontrado nada, y sin embargo las comunidades existen y están en la base. Es un desfase a la hora de contar niveles, no un fallo de detección. Si te sale ese cero, no te asustes.

Y luego está lo que de verdad hace que esto sea sostenible en el tiempo, el build incremental. Cuando lanzo la construcción otra vez, el binario se da cuenta de que solo hay nueve ficheros nuevos o modificados y procesa solo esos, no los seis mil trescientos treinta y cinco. Si este proceso lo metes en un cron y lo lanzas todos los días, el grafo se mantiene al día sin que tú te acuerdes de nada. Ese es el truco para que la cosa no se te vaya de las manos, porque el primer build es el caro (el reconocimiento de entidades va atado al LLM) y los siguientes son casi instantáneos. En el README del proyecto quedan los números, un primer build de cien ficheros rondaba los veinte minutos con un modelo pequeño, seis mil doscientos cincuenta y cuatro ficheros se fueron a unas veintidós horas, y un build incremental de un solo fichero anda por los dos segundos.

Por eso te decía al principio que la lección de mi desastre fue esa. Si indexas de tanto en tanto, no te enteras. Si lo dejas todo para el final, te pasa lo que a mí.

El ejemplo de GIMP y la ventaja de no alucinar

Quiero cerrar la parte de consultas con un caso que me hizo mucha gracia, porque muestra las dos caras de la herramienta. Busco cómo configurar GIMP,

graphrag search "cómo configurar GIMP" grafo.db

Y me devuelve las notas relacionadas, entre ellas una sobre la caja de herramientas de GIMP con estilo de Photoshop. Hasta aquí, perfecto. Pero ahora hago la misma pregunta a ask,

graphrag ask "¿cómo configuro GIMP?" grafo.db

Y el modelo, con el contexto que le he pasado, me responde lo único que había. Que descargue la caja de herramientas y que inicie GIMP. Ni un paso más. Podría haber tirado de memoria y haberme soltado un tutorial genérico de veinte líneas sobre menús y atajos, pero no lo hizo, porque está atado a mis notas y en mis notas no hay más que eso sobre el tema.

Y esa es, para mí, la gran ventaja de todo esto. Aquí no alucina. Es la información que tú le das. Si la tienes, te da información, y si no la tienes, te lo dice.

Puede parecer una limitación, y en cierto sentido lo es. Pero yo lo prefiero mil veces a un asistente que me suelta una respuesta segura y equivocada. Cuando preparo un artículo o el guion de un episodio, esto me dice en qué notas previas me tengo que basar, y si de un tema no tengo nada escrito, me lo dice clarito en lugar de inventarse un hueco que luego tengo que comprobar.

Explorar sin buscar

No siempre tienes una pregunta. A veces tienes un borrador a medias y quieres saber si ya habías hablado de eso. Para esos casos está similar, que funciona al revés que search, en lugar de partir de una consulta escrita, parte de un documento.

Tiene dos modos y la diferencia es la clave. Con --file le pasas un archivo externo que no está indexado, el binario lo lee, lo embebe al vuelo y lo compara contra todo el grafo. Con --label partes de una nota ya indexada y te ahorras ese cálculo.

En la demo tengo un borrador sobre alta disponibilidad con Keepalived que no forma parte del corpus, así que este es el caso de --file,

graphrag similar --file ./borradores/keepalived-alta-disponibilidad.md grafo.db -k 10

Y me devuelve una lista que empieza por Balanceo de carga con Nginx con un 1.000, y su propio texto dice literalmente Keepalived y habla de eliminar el punto único de fallo. Desde ahí la cosa baja rápido y aparece un racimo coherente, Kubernetes, proxy inverso, seguridad en Docker, Fail2ban, hardening. La expansión por grafo ha ido reuniendo todo lo que orbita alrededor de poner Nginx en producción. Para alguien que escribe, esto vale su peso en oro, porque te revela notas relacionadas que ni recordabas y que jamás habrías encontrado con rg, ya que no comparten las palabras.

Y con --label la mecánica es la misma pero partiendo de un nodo existente, lo que sirve para descubrir huecos. Te dice qué notas tienes alrededor y, si esperabas ver alguna que no aparece, sabes que te falta escribirla.

Luego está map, el mapa interactivo en la terminal,

graphrag map grafo.db

Y esto hay que verlo, no me lo contéis. Es una interfaz de terminal con el grafo dibujado con un diseño de fuerzas, como una constelación de nodos. Te mueves con las flechas o con hjkl, como en Vim, seleccionas con Intro y ves los detalles. Pulsas t y vas filtrando por tipo, notas, entidades, tags, conceptos. Pulsas / y buscas un nodo por nombre. Y de repente estás navegando la comunidad de contenedores, saltas a la de redes, y ves con tus propios ojos cómo se conecta todo lo que llevas años escribiendo. Puedes centrarlo en un nodo concreto,

graphrag map grafo.db --from "Nginx" -d 2

Es una de esas cosas que en una captura de pantalla no se aprecian, así que si puedes, pruébala en tu terminal.

Enchufar el grafo a tu asistente con MCP

Y aquí viene la joya de la corona para mí, la integración. El binario expone un servidor MCP, el Model Context Protocol, que es el estándar que usan asistentes como Claude Desktop, Cline o VS Code para hablar con herramientas externas. La analogía oficial es bonita, dicen que MCP es como un USB-C para las aplicaciones de IA, un enchufe único para conectar cualquier herramienta.

Tú arrancas el servidor apuntando a tu base,

graphrag mcp --db grafo.db

Y a partir de ahí, tu asistente puede consultar tu grafo en tiempo real mientras trabajas. El protocolo es JSON-RPC por entrada y salida estándar, el asistente lanza el proceso como un subproceso y se habla con él por stdin y stdout. No hay puerto abierto ni nada expuesto a la red, todo queda en tu máquina. El binario se queda esperando en primer plano, así que mientras la demo está abierta no te devuelve el prompt, para cortarlo, Ctrl+C.

Lo que expone es una caja de herramientas para el asistente. build para construir o actualizar el grafo, search para la búsqueda híbrida, fts para la búsqueda textual exacta, graph para los vecinos de un nodo, path para el camino más corto entre dos nodos, stats para las estadísticas, y en versiones recientes también la detección y el resumen de comunidades y la búsqueda con respuesta narrativa. Además ofrece recursos que el asistente puede leer directamente, como el resumen de estadísticas, la lista de nodos, la de aristas, las notas y las entidades, junto con una plantilla para leer una comunidad concreta por su identificador.

Se enchufa con la configuración de cada cliente, que es siempre la misma idea, un comando y unos argumentos. En el caso de un cliente de escritorio queda así,

{
  "mcpServers": {
    "graphrag": {
      "command": "/usr/local/bin/graphrag",
      "args": ["mcp", "--db", "/home/usuario/kb.db"]
    }
  }
}

Y si quieres verlo hablar sin montar un cliente, le puedes mandar un par de mensajes JSON-RPC a mano y ver las respuestas. Al arrancar, el asistente pide la lista de herramientas y recursos, y a partir de ahí puede responder cosas del estilo de qué notas tengo sobre balanceo o qué camino une dos conceptos, apoyándose en tu grafo y no en su memoria general. Es el mismo grounding de ask, pero esta vez dentro de tu cliente de IA de todos los días.

Sobre la integración con editores, con Neovim ya hay camino hecho. El binario puede generar un plugin de Lua listo para un gestor de plugins, con comandos como buscar relacionadas o insertar un bloque de notas relacionadas directamente en el buffer que estés editando. Y para llevar MCP a Neovim, el cliente de referencia es mcphub.nvim, que se integra con los plugins de chat habituales. Con Obsidian me queda un fleco pendiente, me gustaría tener un bot que consulte el grafo desde el propio vault, y aunque el binario todavía no trae un plugin oficial, la vía existe, se puede conectar un vault a un asistente con un servidor MCP que hable con Obsidian a través de su API local. Es una de esas cosas que me apunto para un episodio futuro.

Alternativas, que no todo es mi binario

No quiero que te lleves la impresión de que esta es la única manera de hacer GraphRAG, porque no lo es. Hay un montón de herramientas estupendas, y comparar ayuda a entender dónde encaja cada una.

La referencia es Microsoft GraphRAG, que es de donde salió todo esto. Es una librería de Python que construye un índice de grafo, detecta comunidades de forma jerárquica y ofrece varios modos de consulta. El global para razonar sobre todo el corpus, el local para razonar sobre entidades concretas expandiendo a sus vecinos, y otros intermedios. Está pensada para corpus grandes, de empresa y de investigación, y su punto fuerte es responder preguntas de conjunto, del estilo de cuáles son los temas principales de este montón de documentos.

LightRAG es otra propuesta interesante. En lugar de expandir por vecinos con una profundidad configurable, hace recuperación a dos niveles, uno fino con entidades y atributos concretos y otro grueso con temas y relaciones amplias, y los combina. Tiene la ventaja de que no depende de resúmenes de comunidad, con lo que gasta menos llamadas al modelo.

nano-graphrag es una versión mínima y legible, unas mil líneas, pensada para copiarla, entenderla y trastearla. Y Neo4j tiene su propio paquete para GraphRAG, con índice vectorial y constructores de grafos de conocimiento, si ya trabajas con ese motor de base de datos.

La diferencia de fondo que quiero que veas es esta. Microsoft GraphRAG y LightRAG son marcos pesados, en Python, orientados a corpus grandes. Mi binario es otra cosa, es un único ejecutable orientado a cuadernos personales, sin Python, sin npm, sin servidores que mantener, con búsqueda instantánea y todo local. No compiten en la misma liga de escala, compiten en la de fricción de instalación. Y el concepto que has aprendido aquí, vectores más grafo, te sirve para cualquiera de ellos.

Cómo lo tengo montado

Te dejo por aquí lo mínimo para que lo pruebes, porque una de las cosas buenas de esto es que se instala en un momento. El binario se instala desde crates,

cargo install graphrag-search

Necesitas Ollama en marcha para los embeddings y para la generación. En mi caso uso un modelo de embeddings y un modelo pequeño de generación, que bastan de sobra para reconocer entidades, resumir comunidades y redactar respuestas. Puedes comprobar que Ollama responde con un curl a su API de modelos.

Y como el arranque tiene su aquel, con comprobar Ollama, descargar los modelos, localizar el binario y verificar la base, te he dejado un script de preparación del entorno en un gist, para que lo copies tal cual y no te pelees con las dependencias. Lo tienes aquí, script de preparación del entorno del episodio 841.

Una advertencia que aprendí a base de golpes, si indexas con un modelo de embeddings y luego arrancas sin config, el binario cae a otro modelo distinto y los vectores no coinciden. Para reproducir tus propias búsquedas hay que respetar la configuración con la que construiste el grafo. Y otra, hay modelos que no se llevan bien con el modo JSON y te devuelven entidades vacías, así que si ves el grafo raro, prueba con otro.

Conclusión

Llegados a este punto, ¿qué conclusión he sacado de estas tres semanas? Pues que la herramienta pequeña que empezamos a construir en el 837 ya está completa y hace algo que hace unos años parecía magia, encontrar en tus propias notas cosas que ni sabías que tenías ahí.

La búsqueda híbrida son tres fases. Primero se parece, con los vectores y la similitud de coseno. Luego se ordena, con el reranking y el dial alfa repartiendo entre significado y conexión. Y después se conecta, con la expansión por grafo que rescata notas que no compartían ni una palabra. Con eso, search te devuelve listas que se leen y se ajustan, y ask da el salto y te responde con fuentes, sin inventar. Y similar, map y el servidor MCP convierten todo esto en algo cotidiano, encontrar borradores relacionados, navegar el grafo a mano y enchufar tus notas a tu asistente.

Si solo te tienes que llevar una idea de todo el episodio, que sea esta, los vectores te dicen qué se parece, el grafo te dice cómo se conecta, y juntos te dicen lo que no sabías que buscabas.

Y te animo a una cosa concreta. Si tienes un repositorio de notas Markdown, aunque sea pequeño, construye tu grafo, lanza un ask y cuéntame en los comentarios si te ha sorprendido algún resultado de esos que aparecen sin compartir ni una palabra con lo que buscabas. A mí me sigue pasando, y es justo lo que me engancha. 🐧


Más información

Deja una respuesta