BUSQUEDA.md 7.64 KB

Lógica del Buscador

Resumen

Buscador instantáneo con Fuse.js en cliente. Busca en clasificadores presupuestarios y navega a vistas de detalle.

Decisiones de Diseño

Motor de búsqueda

  • Tecnología: Fuse.js (cliente)
  • Razón: ~1,400 registros totales (~600 entidades + ~450 objetos + ~380 rubros), cabe en memoria, búsqueda instantánea sin latencia

Configuración Fuse.js

const fuseOptions = {
  keys: [
    { name: 'nombre_normalizado', weight: 0.6 },
    { name: 'sigla_normalizada', weight: 0.2 },
    { name: 'codigo_normalizado', weight: 0.2 }
  ],
  threshold: 0.4,
  distance: 100,
  includeScore: true,
  includeMatches: true,
  minMatchCharLength: 2,
  ignoreLocation: true
};

Tolerancia a errores

  • Normalización de acentos ("andres" → "andrés")
  • Case-insensitive ("UMSA" = "umsa")
  • Fuzzy matching ("univercidad" → "universidad")

Comportamiento

Parámetro Valor
Debounce 250ms
Mínimo caracteres 2
Máximo resultados 15
Navegación Flechas ↑↓ + Enter + scroll de mouse

Estructura del Índice

Índice unificado (implementado)

Todos los clasificadores en un solo array con tipo:

// src/lib/services/search.js
const searchData = [
  // Entidades
  {
    tipo: 'entidad',
    codigo: 139,
    nombre: 'Universidad Mayor De San Andrés',
    sigla: 'UMSA',
    contexto: 'Universidades Públicas',
    años: 20,
    nombre_normalizado: 'universidad mayor de san andres',
    sigla_normalizada: 'umsa',
    codigo_normalizado: '139'
  },
  // Objetos de gasto
  {
    tipo: 'objeto_gasto',
    codigo: '25100',
    nombre: 'Pasajes',
    sigla: null,
    contexto: 'Objeto del Gasto · Partida',
    nivel: 'partida',
    nombre_normalizado: 'pasajes',
    sigla_normalizada: '',
    codigo_normalizado: '25100'
  },
  // Rubros
  {
    tipo: 'rubro',
    codigo: 13000,
    nombre: 'INGRESOS POR IMPUESTOS',
    sigla: null,
    contexto: 'Rubro · Tipo',
    nivel: 'tipo',
    nombre_normalizado: 'ingresos por impuestos',
    sigla_normalizada: '',
    codigo_normalizado: '13000'
  }
]

Fuentes de datos

  • Entidades: tabla ppto.clas_institucional
  • Objetos de gasto: tabla ppto.clas_objetos
  • Rubros: tabla ppto.clas_rubros

Columnas de Búsqueda

Entidades (clas_institucional)

Columna Buscar Mostrar
desc_entidad ✓ nombre principal
sigla_entidad ✓ entre paréntesis
desc_area ✓ contexto
n_gestiones ✓ "X años de datos"
entidad para URL

Objetos de gasto (clas_objetos)

Columna Buscar Mostrar
desc_objeto ✓ nombre principal
objeto ✓ código (monospace)
nivel ✓ contexto ("Objeto del Gasto · Partida")

Rubros (clas_rubros)

Columna Buscar Mostrar
desc_rubros ✓ nombre principal
rubro ✓ código (monospace)
nivel ✓ contexto ("Rubro · Tipo")

Flujo de Búsqueda

1. CARGA INICIAL
   App monta → descarga clasificadores → construye índice Fuse.js

2. USUARIO ESCRIBE
   Input → debounce 250ms → si ≥2 chars → Fuse.search()

3. RESULTADOS
   Fuse devuelve matches → renderizar lista con highlighting

4. SELECCIÓN
   Click o Enter → navegar a URL según tipo

Estructura de URLs

/entidad/[codigo]     →  /entidad/139
/objeto/[codigo]      →  /objeto/25100
/rubro/[codigo]       →  /rubro/13000

Formato de Resultados

Resultados agrupados por tipo de clasificador:

┌─────────────────────────────────────────────────────┐
│ 🔍 [Buscar entidad, objeto de gasto o rubro...]    │
├─────────────────────────────────────────────────────┤
│ ENTIDADES                                           │
├─────────────────────────────────────────────────────┤
│ Universidad Mayor De San Andrés (UMSA)              │
│ Universidades Públicas                              │
├─────────────────────────────────────────────────────┤
│ OBJETOS DEL GASTO                                   │
├─────────────────────────────────────────────────────┤
│ 25100  Pasajes                                      │
│ Objeto del Gasto · Partida                          │
├─────────────────────────────────────────────────────┤
│ RUBROS                                              │
├─────────────────────────────────────────────────────┤
│ 13000  INGRESOS POR IMPUESTOS                       │
│ Rubro · Tipo                                        │
└─────────────────────────────────────────────────────┘

Diferencias visuales

  • Entidades: Nombre + (sigla) en primera línea
  • Objetos de gasto: Código (monospace) + Nombre en primera línea
  • Rubros: Código (monospace) + Nombre en primera línea

Estados del Buscador

Estado Qué mostrar
Vacío (< 2 chars) Placeholder o sugerencias
Cargando índice Spinner / "Cargando..."
Buscando Nada (es instantáneo)
Con resultados Lista de resultados
Sin resultados "No se encontraron resultados para 'xyz'"
Error "Error al cargar datos"

Estructura de Archivos

src/lib/
├── components/
│   └── search/
│       ├── SearchBox.svelte       ← Input + lógica de búsqueda
│       ├── SearchResults.svelte   ← Lista de resultados
│       └── SearchResultItem.svelte ← Item individual
├── services/
│   ├── supabase.js               ← Cliente Supabase
│   └── search.js                 ← Inicialización Fuse.js
├── stores/
│   └── searchStore.js            ← Estado: query, results, loading
├── utils/
│   └── normalize.js              ← Normalización de texto
└── data/
    └── index.js                  ← Carga y construcción del índice

Estado de Implementación

Clasificador Estado Tabla
Entidades ✅ Implementado ppto.clas_institucional
Objetos de gasto ✅ Implementado ppto.clas_objetos
Rubros ✅ Implementado ppto.clas_rubros
Fuente de financiamiento ⏳ Pendiente -
Geográfico ⏳ Pendiente -

Trabajo Futuro

Índice pre-computado

Posibilidad de pre-computar el índice en servidor y servir como JSON estático para mejorar tiempo de carga inicial.

Migración a servidor

Si el índice supera ~20,000 registros:

  1. Habilitar extensión pg_trgm en PostgreSQL
  2. Crear índices GIN en columnas de búsqueda
  3. Cambiar de Fuse.js a queries con similarity() o % operator
  4. El componente de UI se mantiene igual, solo cambia el servicio

Documento de referencia para la implementación del buscador