Sistema de catalogación de libros (PDF/EPUB) con ingesta automática en Apache NiFi, identificación bibliográfica escalonada (1º se prueba mediante metadatos embebidos, 2º Open Library API y 3º LLM), indexación GraphRAG en Neo4j orquestada con Airflow, y consulta en lenguaje natural desde Claude Desktop vía servidor MCP propio.
El sistema integra seis tecnologías orquestadas mediante Docker Compose:
- Apache NiFi 2.4 — ingestión, hash SHA-256, deduplicación y clasificación automática de ficheros (14 procesadores, 4 fases)
- PostgreSQL 16 — catálogo estructurado (
books,files,processing_log) - Pipeline de identificación bibliográfica escalonada — regex → metadatos embebidos → ISBN → Open Library API → LLM (Groq)
- Neo4j 5.26 con índice vectorial — GraphRAG (búsqueda híbrida vector + grafo)
- Apache Airflow 2.9 — orquestación batch de la indexación GraphRAG
- Servidor MCP (FastMCP) — 5 herramientas expuestas a Claude Desktop
El resultado es un sistema capaz de llevar libros técnicos desde su formato original hasta ser consultables en lenguaje natural, con trazabilidad completa de cada etapa del proceso.
| Capa | Componente | Responsabilidad |
|---|---|---|
| Ingesta | Apache NiFi 2.4 | Detección, hash, deduplicación y clasificación de ficheros |
| Almacenamiento | PostgreSQL 16 | Catálogo estructurado: books, files, processing_log |
| Identificación | Scripts Python (1-5) | Identificación bibliográfica escalonada |
| Grafo | Neo4j 5.26 | Nodos Book/Chunk/Concept/Author/Topic con índice vectorial coseno 768D |
| Orquestación | Apache Airflow 2.9 | DAG batch de indexación GraphRAG |
| Embeddings | Ollama + nomic-embed-text | Vectorización de chunks (768 dimensiones) |
| API MCP | FastMCP | Exposición de 5 herramientas para Claude Desktop |
| LLM | Groq (llama-3.3-70b-versatile) | Identificación (último recurso) y extracción de conceptos |
- Fichero depositado en
data/input - NiFi calcula SHA-256 y detecta duplicados contra PostgreSQL
- Clasificación por extensión: epub/mobi → texto, pdf → análisis OCR, otros → rechazado
- Inserción en tabla
filesconstatus='pending' - Scripts de identificación ejecutados en orden de coste ascendente
- Resultado persistido en
booksconmetadata_srcyconfidence - Fichero renombrado y movido a
data/processed/{inicial}/{autor}/fichero.ext - DAG de Airflow indexa libros pendientes en Neo4j (chunking + embeddings + conceptos)
- Servidor MCP expone las 5 herramientas a Claude Desktop
Todos los contenedores llevan el sufijo _CL para evitar conflictos
con otros contenedores Docker que puedas tener corriendo. Se comunican
entre sí usando el container_name como hostname — ya configurado en
docker-compose.yml.
| Contenedor | Imagen | Puerto | Función |
|---|---|---|---|
postgres_CL |
postgres:16-alpine | 5432 | Base de datos relacional (bookcat + airflow_meta) |
nifi_CL |
apache/nifi (custom) | 8443 | Ingesta y clasificación de ficheros |
neo4j_CL |
neo4j:5.26-community | 7474 / 7687 | Grafo de conocimiento con índice vectorial |
ollama_CL |
ollama/ollama | 11434 | Embeddings locales (nomic-embed-text) |
airflow_CL |
apache/airflow (custom) | 8080 | Orquestación del DAG de indexación |
mcp-server_CL |
custom FastMCP | 3001 | Servidor MCP (modo SSE) para pruebas |
# 1. Levantar todos los servicios
docker compose up -d --build
# 2. Descargar el modelo de embeddings en Ollama (obligatorio)
docker exec ollama_CL ollama pull nomic-embed-text
# 3. Configurar la API key de Groq (identificación LLM + extracción de conceptos)
export GROQ_API_KEY="tu-api-key-de-groq"
# 4. Reiniciar para que los servicios lean la variable
docker compose up -d
# 5. Verificar accesos
# PostgreSQL: psql -h localhost -U catalog -d bookcat
# NiFi: https://localhost:8443/nifi (admin / admin1234admin)
# Neo4j: http://localhost:7474 (neo4j / neo4j1234)
# Ollama: curl http://localhost:11434/api/tags
# Airflow: http://localhost:8080 (admin / admin)Nota sobre el LLM: el diseño original del proyecto pedía usar la API de Claude para identificación y extracción de conceptos. Durante el desarrollo se migró a Groq (
llama-3.3-70b-versatile) por su capa gratuita más generosa.docker-compose.ymlsigue reservandoGEMINI_API_KEY,ANTHROPIC_API_KEYyOPENAI_API_KEYcomo variables de entorno para una futura extensión multi-proveedor, pero ningún script las usa actualmente — la única variable que el código necesita esGROQ_API_KEY. Sin ella, el sistema sigue funcionando pero sin identificación por LLM ni extracción de conceptos (ambas son el último eslabón de sus respectivos pipelines).
books— id, title, authors, isbn, year, publisher, language, summary, main_topic, subtopics, tags, metadata_src, confidence, graphrag_indexedfiles— id, book_id, file_hash, file_size, extension, original_name, stored_path, statusprocessing_log— id, file_id, stage, message, result, created_at
Índice único sobre file_hash para deduplicación eficiente.
| Nodo | Propiedades | Descripción |
|---|---|---|
Book |
pg_id, title, authors, isbn, year, publisher | Espejo del libro en PostgreSQL |
Chunk |
id, text, position, page, embedding[768] | Fragmento de 750 tokens con vector coseno |
Concept |
name, description, domain | Entidad extraída por LLM |
Author |
name | Autor del libro |
Topic |
name | Tema principal |
Relaciones: Book -[HAS_CHUNK]-> Chunk, Chunk -[MENTIONS]-> Concept,
Book -[WRITTEN_BY]-> Author, Book -[ABOUT]-> Topic/Concept.
Índice vectorial chunk_embeddings (768 dimensiones, similaridad coseno).
El flujo de NiFi (docs/NiFi-flow.json) implementa 4 fases: A
ingesta y hash, B deduplicación, C clasificación por formato,
D registro e identificación.
La identificación bibliográfica se resuelve en cascada, deteniéndose en cuanto la confianza es suficiente:
| Script | Método | Confianza | Activación |
|---|---|---|---|
identify_filename.py |
Regex sobre nombre del fichero | 0.1 – 0.7 | Siempre |
identify_metadata.py |
PyPDF2 / ebooklib (metadatos embebidos) | 0.5 – 0.8 | Si confianza < 0.8 |
identify_isbn.py |
Regex ISBN-10/13 con validación de dígito de control | 0.9 | Siempre (extrae ISBN) |
identify_openlibrary.py |
API Open Library por ISBN o título+autor | 0.75 – 0.95 | Si hay ISBN o confianza < 0.9 |
identify_llm.py |
Groq llama-3.3-70b (fallback) | 0.6 – 0.8 | Si confianza < 0.5 |
Organización física resultante:
data/processed/{inicial}/{autor_normalizado}/{Autor - Título - Año - Editorial.ext}
El DAG graphrag_indexing procesa hasta 10 libros pendientes por
ejecución (schedule_interval="0 2 * * *"). Para cada libro:
extract_text.py— extrae texto por páginas (PyPDF2 / ebooklib)chunk_text.py— divide en chunks de 750 tokens con solapamiento de 100generate_embeddings.py— genera embeddings 768D connomic-embed-textvía Ollamainsert_neo4j.py— crea nodosBook,Chunk,Author,Topicy relacionesextract_concepts.py— extrae conceptos con Groq, muestreando cada 5 chunks
El servidor MCP (scripts/mcp/server.py),
implementado con FastMCP, expone 5 herramientas:
| Tool | Backend | Descripción |
|---|---|---|
search_books |
PostgreSQL ILIKE |
Búsqueda por texto libre con filtros opcionales (tema, autor, idioma, año) |
book_detail |
PostgreSQL JOIN |
Ficha completa del libro con ruta física del fichero |
ask_content |
Neo4j vector + grafo | Embedding de la pregunta → búsqueda vectorial → traversal de conceptos |
find_concepts |
Neo4j traversal | Búsqueda por nombre de concepto con libros y fragmentos asociados |
compare_books |
Neo4j vector filtrado | Búsqueda vectorial por libro con similaridad coseno directa |
Modos de ejecución: SSE (puerto 3001, Docker) para pruebas y stdio (venv local) para Claude Desktop.
cd scripts/mcp
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
# Copiar config/claude_desktop_config.example.json al directorio de Claude Desktop
# Ajustar rutas y reiniciar Claude DesktopDemostración de las 5 herramientas conectadas a Claude Desktop:
Vídeo completo (mejor calidad): docs/Claude_desktop_MCP_test.mp4
Cifras de una ejecución de validación con un catálogo de prueba de 10 libros (no un despliegue en producción):
| Métrica | Valor |
|---|---|
| Libros catalogados / ficheros procesados | 10 / 10 |
Identificados vía openlibrary_isbn |
7 (70%) |
| Identificados vía metadatos embebidos | 2 (20%) |
| Identificados vía LLM | 1 (10%) |
Libros indexados en Neo4j (graphrag_indexed=true) |
10 (100%) |
| Nodos Chunk / Concept / Author en Neo4j | 2280 / 557 / 9 |
| Relaciones HAS_CHUNK / MENTIONS / ABOUT | 2280 / 686 / 573 |
Tiempos de respuesta del servidor MCP: search_books y book_detail
< 100ms; find_concepts < 500ms; ask_content 1-3s; compare_books 2-5s.
Deuda técnica conocida:
- El campo
languagese detecta de forma inconsistente para libros en inglés (Open Library no siempre lo devuelve) main_topicysubtopicsquedan vacíos cuando no se alcanza el umbral de confianza necesario para invocar al LLM- El directorio temporal
/tmp/graphrag_Nno se limpia automáticamente tras la indexación
Próximos pasos (corto/medio/largo plazo):
- Migrar
identify_llm.pyde Groq a la API de Claude (requisito original del proyecto) - Reindexación incremental en Neo4j (hoy requiere borrar y reindexar)
- Autenticación en el servidor MCP para entornos multiusuario
- Tool
list_booksy paginación ensearch_books - Chunking con detección de capítulos/secciones para mejorar el
linaje de
ask_content
proyecto/
├── docker-compose.yml
├── README.md
├── dockerfiles/
│ ├── nifi/Dockerfile + requirements.txt
│ └── airflow/Dockerfile + requirements.txt
├── sql/init.sql
├── scripts/
│ ├── identify_*.py # Pipeline de identificación bibliográfica (1-5)
│ ├── move_to_processed.py
│ ├── nifi/ # Scripts Python invocados desde NiFi
│ ├── graphrag/ # Indexación GraphRAG (Airflow)
│ └── mcp/ # Servidor MCP (server.py + Dockerfile)
├── airflow/dags/dag_graphrag.py
├── data/ # input/ pending_ocr/ processed/ rejected/
├── config/ # claude_desktop_config.example.json
└── docs/ # diagramas, capturas y demo del MCP
- Mínimo: 8 GB RAM, 4 cores, 20 GB disco
- Recomendado: 16 GB RAM, SSD
- GPU: no necesaria — el único modelo local (
nomic-embed-textpara embeddings) va bien en CPU; la identificación/extracción de conceptos usa la API de Groq
Todas las credenciales están fijadas en docker-compose.yml y
sql/init.sql para simplificar el arranque en local. No se usan fuera
de este entorno de desarrollo.
| Servicio | Usuario | Contraseña |
|---|---|---|
| PostgreSQL | catalog | catalog1234 |
| NiFi | admin | admin1234admin |
| Neo4j | neo4j | neo4j1234 |
| Airflow | admin | admin |
El fichero sql/init.sql se ejecuta automáticamente en el primer
arranque de PostgreSQL. La imagen oficial detecta que el volumen
pg_data está vacío y ejecuta todos los .sql de
/docker-entrypoint-initdb.d/ (donde está montada la carpeta sql/).
En arranques posteriores, PostgreSQL ignora ese directorio. Si
modificas init.sql después del primer arranque, tienes dos opciones:
- Ejecutar los cambios manualmente:
psql -h localhost -U catalog -d bookcat -f sql/init.sql - Borrar el volumen y reiniciar desde cero:
docker compose down -v && docker compose up -d
Basado en un trabajo de Antonio Javier Calvo Torrejón.






