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