Reestructuración modular integral de servicios, reorganización de gestión en docs/ y limpieza profunda del repositorio
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/analizar_esquema_busqueda.rb — Análisis de esquema para sistema de búsqueda
|
||||
# ================================================================================
|
||||
# Este script analiza el esquema de la base de datos dtic-BITACORAs para diseñar
|
||||
# el sistema de búsqueda y filtrado (Tarea 2.2 del Plan ADN BITACORAs).
|
||||
#
|
||||
# Uso:
|
||||
# ruby tools/analizar_esquema_busqueda.rb
|
||||
#
|
||||
# Salida:
|
||||
# • Estructura completa de tablas y columnas
|
||||
# • Índices existentes
|
||||
# • Relaciones entre tablas
|
||||
# • Campos recomendados para búsqueda
|
||||
# • Índices recomendados para optimización
|
||||
|
||||
require 'pg'
|
||||
require 'yaml'
|
||||
|
||||
class AnalizadorEsquemaBusqueda
|
||||
def initialize
|
||||
@config = cargar_configuracion
|
||||
@conn = nil
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
conectar_bd
|
||||
analizar_esquema
|
||||
ensure
|
||||
desconectar_bd
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cargar_configuracion
|
||||
config_path = File.join(__dir__, 'config', 'database.yml')
|
||||
unless File.exist?(config_path)
|
||||
puts "❌ Archivo de configuración no encontrado: #{config_path}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
config = YAML.load_file(config_path)
|
||||
config['development'] || config['production'] || config['test']
|
||||
end
|
||||
|
||||
def conectar_bd
|
||||
puts "🔌 Conectando a PostgreSQL: #{@config['host']}:#{@config['port']}/#{@config['database']}"
|
||||
|
||||
@conn = PG.connect(
|
||||
host: @config['host'],
|
||||
port: @config['port'],
|
||||
dbname: @config['database'],
|
||||
user: @config['username'],
|
||||
password: @config['password']
|
||||
)
|
||||
|
||||
puts "✅ Conexión establecida\n\n"
|
||||
rescue PG::Error => e
|
||||
puts "❌ Error de conexión: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
def desconectar_bd
|
||||
@conn&.close
|
||||
puts "\n🔌 Conexión cerrada"
|
||||
end
|
||||
|
||||
def analizar_esquema
|
||||
puts "📊 ANÁLISIS DE ESQUEMA PARA SISTEMA DE BÚSQUEDA"
|
||||
puts "=" * 70
|
||||
|
||||
# Tablas principales del sistema
|
||||
tablas = ['nodos', 'bitacoras', 'temas', 'entradas']
|
||||
|
||||
tablas.each do |tabla|
|
||||
analizar_tabla(tabla)
|
||||
end
|
||||
|
||||
analizar_relaciones
|
||||
analizar_campos_busqueda
|
||||
recomendar_indices
|
||||
diseñar_sistema_busqueda
|
||||
end
|
||||
|
||||
def analizar_tabla(nombre_tabla)
|
||||
puts "\n🔍 TABLA: #{nombre_tabla}"
|
||||
puts "-" * 50
|
||||
|
||||
# Obtener columnas
|
||||
columnas = @conn.exec_params(
|
||||
"SELECT column_name, data_type, character_maximum_length,
|
||||
is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1 AND table_schema = 'bitacoras'
|
||||
ORDER BY ordinal_position",
|
||||
[nombre_tabla]
|
||||
)
|
||||
|
||||
puts " 📋 Columnas (#{columnas.ntuples}):"
|
||||
columnas.each do |col|
|
||||
tipo = col['data_type']
|
||||
longitud = col['character_maximum_length'] ? "(#{col['character_maximum_length']})" : ''
|
||||
nulo = col['is_nullable'] == 'YES' ? 'NULL' : 'NOT NULL'
|
||||
defecto = col['column_default'] ? " [DEFAULT: #{col['column_default']}]" : ''
|
||||
puts " • #{col['column_name']}: #{tipo}#{longitud} #{nulo}#{defecto}"
|
||||
end
|
||||
|
||||
# Obtener índices
|
||||
indices = @conn.exec_params(
|
||||
"SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = $1 AND schemaname = 'bitacoras'
|
||||
ORDER BY indexname",
|
||||
[nombre_tabla]
|
||||
)
|
||||
|
||||
puts "\n 🔗 Índices (#{indices.ntuples}):"
|
||||
if indices.ntuples > 0
|
||||
indices.each do |idx|
|
||||
definicion = idx['indexdef']
|
||||
.gsub('CREATE ', '')
|
||||
.gsub(" ON bitacoras.#{nombre_tabla} USING ", ' → ')
|
||||
puts " • #{idx['indexname']}: #{definicion}"
|
||||
end
|
||||
else
|
||||
puts " • (sin índices)"
|
||||
end
|
||||
|
||||
# Contar registros
|
||||
conteo = @conn.exec_params(
|
||||
"SELECT COUNT(*) as total FROM bitacoras.#{nombre_tabla}",
|
||||
[]
|
||||
)[0]['total']
|
||||
|
||||
puts "\n 📊 Registros: #{conteo}"
|
||||
end
|
||||
|
||||
def analizar_relaciones
|
||||
puts "\n🔗 RELACIONES ENTRE TABLAS"
|
||||
puts "-" * 50
|
||||
|
||||
query = <<-SQL
|
||||
SELECT
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM
|
||||
information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
WHERE
|
||||
tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = 'bitacoras'
|
||||
ORDER BY tc.table_name, kcu.column_name;
|
||||
SQL
|
||||
|
||||
relaciones = @conn.exec(query)
|
||||
|
||||
if relaciones.ntuples > 0
|
||||
relaciones.each do |rel|
|
||||
puts " • #{rel['table_name']}.#{rel['column_name']} → #{rel['foreign_table_name']}.#{rel['foreign_column_name']}"
|
||||
end
|
||||
else
|
||||
puts " • (no se encontraron relaciones explícitas con FOREIGN KEY)"
|
||||
end
|
||||
|
||||
# Análisis de relaciones implícitas
|
||||
puts "\n 🔍 Relaciones implícitas (por nombre de columna):"
|
||||
puts " • entradas.nodo_id → nodos.id"
|
||||
puts " • entradas.tema_id → temas.id"
|
||||
puts " • entradas.bitacora_id → bitacoras.id"
|
||||
puts " • temas.nodo_id → nodos.id"
|
||||
end
|
||||
|
||||
def analizar_campos_busqueda
|
||||
puts "\n🎯 CAMPOS PARA BÚSQUEDA Y FILTRADO"
|
||||
puts "-" * 50
|
||||
|
||||
campos_por_tabla = {
|
||||
'nodos' => [
|
||||
{nombre: 'nombre', tipo: 'texto', busqueda: 'exacta/parcial', filtro: 'si'},
|
||||
{nombre: 'tipo', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
|
||||
{nombre: 'descripcion', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
|
||||
{nombre: 'estado', tipo: 'texto', busqueda: 'exacta', filtro: 'si'}
|
||||
],
|
||||
'bitacoras' => [
|
||||
{nombre: 'fecha', tipo: 'date', busqueda: 'rango', filtro: 'si'},
|
||||
{nombre: 'resumen', tipo: 'texto', busqueda: 'full-text', filtro: 'no'}
|
||||
],
|
||||
'temas' => [
|
||||
{nombre: 'nombre', tipo: 'texto', busqueda: 'exacta/parcial', filtro: 'si'},
|
||||
{nombre: 'descripcion', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
|
||||
{nombre: 'estado', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
|
||||
{nombre: 'modo', tipo: 'texto', busqueda: 'exacta', filtro: 'si'}
|
||||
],
|
||||
'entradas' => [
|
||||
{nombre: 'inicio', tipo: 'time', busqueda: 'rango', filtro: 'si'},
|
||||
{nombre: 'fin', tipo: 'time', busqueda: 'rango', filtro: 'si'},
|
||||
{nombre: 'descripcion', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
|
||||
{nombre: 'detalle', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
|
||||
{nombre: 'estado', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
|
||||
{nombre: 'modo', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
|
||||
{nombre: 'created_at', tipo: 'timestamp', busqueda: 'rango', filtro: 'si'}
|
||||
]
|
||||
}
|
||||
|
||||
campos_por_tabla.each do |tabla, campos|
|
||||
puts "\n 📋 #{tabla}:"
|
||||
campos.each do |campo|
|
||||
# Verificar si el campo existe
|
||||
existe = @conn.exec_params(
|
||||
"SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = $1 AND table_schema = 'bitacoras'
|
||||
AND column_name = $2",
|
||||
[tabla, campo[:nombre]]
|
||||
)
|
||||
|
||||
if existe.ntuples > 0
|
||||
icono = campo[:filtro] == 'si' ? '🔍' : '📝'
|
||||
puts " #{icono} #{campo[:nombre]} (#{campo[:tipo]}) - Búsqueda: #{campo[:busqueda]}, Filtro: #{campo[:filtro]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def recomendar_indices
|
||||
puts "\n💡 ÍNDICES RECOMENDADOS PARA OPTIMIZACIÓN"
|
||||
puts "-" * 50
|
||||
|
||||
puts " 1. Para búsqueda full-text:"
|
||||
puts " • CREATE INDEX idx_entradas_descripcion_fts ON bitacoras.entradas"
|
||||
puts " USING gin(to_tsvector('spanish', descripcion));"
|
||||
puts " • CREATE INDEX idx_entradas_detalle_fts ON bitacoras.entradas"
|
||||
puts " USING gin(to_tsvector('spanish', detalle));"
|
||||
puts " • CREATE INDEX idx_temas_descripcion_fts ON bitacoras.temas"
|
||||
puts " USING gin(to_tsvector('spanish', descripcion));"
|
||||
|
||||
puts "\n 2. Para filtrado frecuente:"
|
||||
puts " • CREATE INDEX idx_entradas_estado ON bitacoras.entradas(estado);"
|
||||
puts " • CREATE INDEX idx_entradas_modo ON bitacoras.entradas(modo);"
|
||||
puts " • CREATE INDEX idx_entradas_created_at ON bitacoras.entradas(created_at);"
|
||||
puts " • CREATE INDEX idx_bitacoras_fecha ON bitacoras.bitacoras(fecha);"
|
||||
puts " • CREATE INDEX idx_nodos_tipo ON bitacoras.nodos(tipo);"
|
||||
|
||||
puts "\n 3. Para joins y relaciones:"
|
||||
puts " • CREATE INDEX idx_entradas_nodo_id ON bitacoras.entradas(nodo_id);"
|
||||
puts " • CREATE INDEX idx_entradas_tema_id ON bitacoras.entradas(tema_id);"
|
||||
puts " • CREATE INDEX idx_entradas_bitacora_id ON bitacoras.entradas(bitacora_id);"
|
||||
puts " • CREATE INDEX idx_temas_nodo_id ON bitacoras.temas(nodo_id);"
|
||||
end
|
||||
|
||||
def diseñar_sistema_busqueda
|
||||
puts "\n🚀 DISEÑO DEL SISTEMA DE BÚSQUEDA Y FILTRADO"
|
||||
puts "-" * 50
|
||||
|
||||
puts " 1. 📋 Comandos CLI propuestos:"
|
||||
puts " • adn db buscar:entradas [TEXTO] [OPCIONES]"
|
||||
puts " • adn db filtrar:entradas [--estado ESTADO] [--modo MODO] [--fecha DESDE-HASTA]"
|
||||
puts " • adn db buscar:nodos [TEXTO] [--tipo TIPO]"
|
||||
puts " • adn db buscar:temas [TEXTO] [--nodo NODO]"
|
||||
puts " • adn db estadisticas:busquedas [--periodo PERIODO]"
|
||||
|
||||
puts "\n 2. 🔧 Opciones de búsqueda avanzada:"
|
||||
puts " • --exacto: Búsqueda exacta (no parcial)"
|
||||
puts " • --campo CAMPO: Buscar solo en campo específico"
|
||||
puts " • --limite N: Limitar resultados"
|
||||
puts " • --ordenar CAMPO: Ordenar por campo (fecha, estado, etc.)"
|
||||
puts " • --formato FORMATO: Salida en texto, JSON o CSV"
|
||||
|
||||
puts "\n 3. 📊 Tipos de búsqueda implementados:"
|
||||
puts " • Full-text search: PostgreSQL tsvector/tsquery"
|
||||
puts " • Búsqueda parcial: ILIKE '%texto%'"
|
||||
puts " • Búsqueda exacta: = 'texto'"
|
||||
puts " • Filtrado por rango: BETWEEN, >, <"
|
||||
puts " • Filtrado por lista: IN (...)"
|
||||
|
||||
puts "\n 4. 🏗️ Arquitectura del módulo:"
|
||||
puts " • Clase BuscadorBD: Lógica central de búsqueda"
|
||||
puts " • Clase FiltrosBD: Sistema de filtrado"
|
||||
puts " • Clase ExportadorResultados: Formatos de salida"
|
||||
puts " • Integración con BitacoraDB existente"
|
||||
|
||||
puts "\n 5. 📈 Métricas de rendimiento:"
|
||||
puts " • Tiempo de respuesta < 500ms para 10k registros"
|
||||
puts " • Cache de consultas frecuentes"
|
||||
puts " • Paginación automática (100 resultados/página)"
|
||||
end
|
||||
end
|
||||
|
||||
# Ejecutar análisis
|
||||
if __FILE__ == $0
|
||||
begin
|
||||
analizador = AnalizadorEsquemaBusqueda.new
|
||||
analizador.ejecutar
|
||||
rescue => e
|
||||
puts "❌ Error durante el análisis: #{e.message}"
|
||||
puts e.backtrace if ENV['DEBUG']
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user