308 lines
11 KiB
Ruby
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
|