>
arrow_back Prototipos
RAG jurídico
En vivo

Tribunal de Contratación Pública

¿Se puede hacer búsqueda jurídica útil sobre miles de sentencias en PDF, sin pagar una base de datos legal?

FastAPI · SQLite FTS5 · BM25+TF-IDF · 5.995 causas · :8007

Estado
En vivo · scrape incremental
Fuente de datos
Tribunal de Contratación Pública (portal)
Stack
FastAPI · SQLite FTS5 · BM25+TF-IDF · pdfplumber
Cadencia
Incremental · solo lo nuevo
Cobertura
5.995 causas · 1.716 sentencias · 1.531 PDF
Infraestructura
Raspberry Pi · $0 cloud

El problema

El Tribunal de Contratación Pública resuelve las impugnaciones contra las grandes licitaciones del Estado. Su jurisprudencia es pública, pero vive dispersa en un portal donde la sección de sentencias quedó desactualizada por años y cada fallo es un PDF suelto, sin buscador real por contenido. Quise responder una pregunta concreta: ¿cuánto cuesta — en plata y en código — tener esa jurisprudencia indexada al día y buscable por lo que dice cada sentencia, no solo por su número de rol?

Qué construí

Prototipé un monitor que scrapea las causas y sentencias del Tribunal, descarga los PDFs, extrae su texto y los deja buscables desde un dashboard. Hoy el índice tiene 5.995 causas (2008–2026), 1.716 sentencias y 58.314 trámites; de las sentencias, 1.531 tienen el PDF descargado y su texto indexado.

  • check_circle Un scraper reconstruye la sesión del portal del Tribunal y recorre sus APIs internas para traer causas, trámites y sentencias, con sus metadatos (rol, carátula, materia, mecanismo, demandante, demandado, licitación asociada).
  • check_circle Cada sentencia se descarga en PDF y se le extrae el texto con pdfplumber, que es lo que después permite buscar por contenido y mostrar extractos del fallo.
  • check_circle La búsqueda es híbrida: combina BM25 (palabras) con TF-IDF y fusiona ambos rankings con Reciprocal Rank Fusion, además de FTS5 de SQLite. Hay también una búsqueda avanzada por campos (demandante, demandado, rol, licitación).
  • check_circle Un scheduler vuelve a pasar de forma incremental para capturar las sentencias nuevas, sin re-descargar todo. Corre 24/7 sobre una Raspberry Pi, con $0 de costo cloud.

Cómo funciona

El backend (FastAPI) mantiene la base en SQLite con índices FTS5 y reconstruye en memoria el índice de búsqueda al arrancar. La API expone el listado de jurisprudencia, el detalle de cada sentencia con su texto y trámites, y el buscador con filtros por año, materia y mecanismo. Apache hace de proxy hacia el servicio interno bajo /tribunal.

Decisiones técnicas

Búsqueda híbrida BM25 + TF-IDF con fusión de rankings

Combiné dos rankings léxicos y los fusioné con Reciprocal Rank Fusion en vez de quedarme con uno solo. Cada método falla distinto; fusionarlos da resultados más robustos sin el costo ni la dependencia de un modelo de embeddings.

Indexar el texto del PDF, asumiendo que es imperfecto

Extraigo con pdfplumber sabiendo que algunos fallos quedan con texto incompleto. El detalle siempre enlaza al PDF original como fuente de verdad: mejor un índice imperfecto y honesto que ningún buscador por contenido.

Scrape incremental, no full cada vez

Reconstruyo la sesión del portal y traigo solo lo nuevo. Mantiene el índice al día —la sección oficial de sentencias estuvo años desactualizada— sin re-descargar miles de PDFs en cada corrida.

Qué es real y qué es parcial

Es un prototipo, no una base de datos legal oficial — la fuente de verdad es siempre la sentencia original.

  • verified Real: causas, sentencias y trámites vienen del propio portal del Tribunal; 1.531 sentencias tienen el PDF descargado y su texto indexado.
  • schedule Parcial: la extracción de texto desde PDF no es perfecta —algunos fallos quedan incompletos— y la búsqueda es léxica, no entiende sinónimos jurídicos. El detalle siempre enlaza a la sentencia original.
  • build Limitación heredada: si en el portal oficial algo falta o llega tarde, acá también. No reemplaza a la fuente oficial.

¿Quieres buscar en la jurisprudencia?

El buscador corre ahora mismo sobre la Pi.

Abrir el prototipo arrow_forward