Base de datos
BCN Extractor usa PostgreSQL 15+ como base de datos principal. El esquema está diseñado para soportar búsqueda full-text en español, consultas JSONB y detección eficiente de cambios.
Tablas
Section titled “Tablas”tipos_normas — Catálogo de tipos
Section titled “tipos_normas — Catálogo de tipos”Tabla de referencia con los tipos de normas que maneja la BCN (Decreto, Ley, Resolución, etc.).
CREATE TABLE tipos_normas ( id SERIAL PRIMARY KEY, nombre TEXT NOT NULL UNIQUE, abreviatura TEXT, fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, fecha_actualizacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP);instituciones — Instituciones de la BCN
Section titled “instituciones — Instituciones de la BCN”Almacena las instituciones públicas cuyos cuerpos normativos están en la BCN.
CREATE TABLE instituciones ( id INTEGER PRIMARY KEY, -- ID propio de la BCN nombre TEXT NOT NULL, fecha_agregada TIMESTAMP, fecha_actualizada TIMESTAMP);normas — Tabla central
Section titled “normas — Tabla central”La tabla principal del sistema. Almacena metadatos, contenido y referencias a los archivos en disco.
CREATE TABLE normas ( id INTEGER PRIMARY KEY, -- ID propio de la BCN id_tipo INTEGER REFERENCES tipos_normas(id), numero VARCHAR(50), titulo TEXT, estado VARCHAR(20) DEFAULT 'vigente', -- 'vigente' | 'derogada' fecha_publicacion DATE, fecha_promulgacion DATE, organismo TEXT, xml_path TEXT, -- ruta al XML en disco md_path TEXT, -- ruta al Markdown en disco contenido_texto TEXT, -- texto completo para FTS metadata_json JSONB, -- materias, organismos, flags hash_xml VARCHAR(32), -- MD5 para detectar cambios fecha_descarga TIMESTAMP DEFAULT CURRENT_TIMESTAMP, fecha_actualizacion TIMESTAMP);normas_instituciones — Relación muchos-a-muchos
Section titled “normas_instituciones — Relación muchos-a-muchos”Una norma puede pertenecer a múltiples instituciones y una institución puede tener múltiples normas.
CREATE TABLE normas_instituciones ( id_norma INTEGER REFERENCES normas(id), id_institucion INTEGER REFERENCES instituciones(id), fecha_asociacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id_norma, id_institucion));descargas — Log de operaciones
Section titled “descargas — Log de operaciones”Registra cada intento de descarga o sincronización para trazabilidad y debugging.
CREATE TABLE descargas ( id SERIAL PRIMARY KEY, id_norma INTEGER REFERENCES normas(id), tipo_descarga VARCHAR(50), -- 'descarga' | 'sincronizacion' estado VARCHAR(50), -- 'exitosa' | 'error' fecha_intento TIMESTAMP, error_mensaje TEXT);Índices y optimizaciones
Section titled “Índices y optimizaciones”Full-text search en español
Section titled “Full-text search en español”-- Índice GIN sobre el campo titulo para FTS en españolCREATE INDEX idx_normas_titulo_ftsON normasUSING GIN (to_tsvector('spanish', titulo));
-- Consulta de ejemploSELECT id, tituloFROM normasWHERE to_tsvector('spanish', titulo) @@ to_tsquery('spanish', 'medioambiente')ORDER BY ts_rank(to_tsvector('spanish', titulo), to_tsquery('spanish', 'medioambiente')) DESC;Índices B-tree
Section titled “Índices B-tree”CREATE INDEX idx_normas_id_tipo ON normas(id_tipo);CREATE INDEX idx_normas_estado ON normas(estado);CREATE INDEX idx_tipos_nombre ON tipos_normas(nombre);Índice GIN sobre JSONB
Section titled “Índice GIN sobre JSONB”-- Para consultas por claves dentro de metadata_jsonCREATE INDEX idx_normas_metadata ON normas USING GIN (metadata_json);
-- Consulta de ejemplo: normas sobre una materia específicaSELECT id, titulo FROM normasWHERE metadata_json @> '{"materias": ["salud"]}';Detección de cambios via MD5
Section titled “Detección de cambios via MD5”El campo hash_xml almacena el MD5 del XML descargado. En cada sincronización:
- Se descarga el XML de la BCN
- Se calcula su MD5
- Si el MD5 coincide con el almacenado → se omite el upsert
- Si difiere → se actualiza la norma y su hash
Esto hace que las sincronizaciones incrementales sean muy rápidas en instituciones con pocas normas modificadas.
Consultas útiles
Section titled “Consultas útiles”-- Estadísticas generalesSELECT (SELECT COUNT(*) FROM normas) AS total_normas, (SELECT COUNT(*) FROM instituciones) AS total_instituciones, (SELECT COUNT(*) FROM tipos_normas) AS total_tipos, (SELECT COUNT(*) FROM normas WHERE estado = 'vigente') AS vigentes, (SELECT COUNT(*) FROM normas WHERE estado = 'derogada') AS derogadas;
-- Normas por tipoSELECT t.nombre, COUNT(n.id) AS totalFROM tipos_normas tLEFT JOIN normas n ON n.id_tipo = t.idGROUP BY t.nombreORDER BY total DESC;
-- Instituciones con más normasSELECT i.nombre, COUNT(ni.id_norma) AS total_normasFROM instituciones iJOIN normas_instituciones ni ON ni.id_institucion = i.idGROUP BY i.nombreORDER BY total_normas DESCLIMIT 20;
-- Últimas normas descargadasSELECT id, titulo, fecha_descargaFROM normasORDER BY fecha_descarga DESCLIMIT 10;Conexión directa
Section titled “Conexión directa”Puedes conectarte con cualquier cliente PostgreSQL usando las credenciales de tu .env:
# psqlpsql -h localhost -p 5432 -U postgres -d bcn_extractor
# Docker execdocker exec -it bcn_extractor_db psql -U postgres -d bcn_extractor