Files
dtic-DIIAA/tools/analizar_esquema_busqueda.rb
T

308 lines
11 KiB
Ruby

#!/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