ADR-014: Búsqueda con PostgreSQL FTS (no Elasticsearch)¶
Estado: Aceptado Fecha: 2026-06-21 Autores: Giampiero (mantenedor principal)
Contexto¶
La Fase 2 (E09 — búsqueda, consulta y reportes) exige búsqueda avanzada sobre radicados y expedientes: texto completo con ranking, coincidencia difusa/prefijo, filtros por campos (incluidos metadatos JSONB), rangos de fecha, y reportes/estadísticas. El SGDEA debe permitir que un operador encuentre cualquier radicado/expediente.
El motor de almacenamiento es PostgreSQL con aislamiento por schema tenant_{slug} (ADR-002) y acceso con asyncpg + SQL crudo (ADR-003). La base ya tiene búsqueda full-text nativa funcionando: columna generada search_vector tsvector con setweight/to_tsvector('spanish', …), índice GIN, e índices trigram (pg_trgm) para coincidencia difusa (document-service/migrations/tenant/003_search_indexes.sql, routers/search.py con websearch_to_tsquery + ts_rank + fallback ILIKE/trigram). Los metadatos variables viven en metadata JSONB con índice GIN (ADR-007).
La pregunta es si introducir Elasticsearch/OpenSearch como motor de búsqueda dedicado, o consolidar sobre las capacidades nativas de PostgreSQL.
Decisión¶
Implementar la búsqueda de OrpycaMCP sobre PostgreSQL nativo (FTS tsvector + pg_trgm + filtros JSONB/GIN), sin un motor de búsqueda externo en F2.
- Texto completo:
to_tsvector('spanish', …)con pesos (A=número de radicado,B=asunto, …), consulta conwebsearch_to_tsquery, ranking conts_rank/ts_rank_cd, índice GIN sobresearch_vector. - Difuso/prefijo:
pg_trgm(índices GINgin_trgm_ops) yunaccentpara normalizar acentos. - Filtros estructurados y por metadato:
WHEREsobre columnas + contenciónmetadata @> $jsonb(índice GIN), rangos de fecha, estado, dependencia, tipo. - Reportes/estadísticas: agregaciones SQL (
GROUP BY,COUNT, ventanas) sobre el schema del tenant. - El índice es derivado de la BD (la fila es la verdad): no se mantiene un store paralelo que pueda divergir.
Consecuencias¶
Positivas:
- Cero infraestructura nueva: sin cluster Elasticsearch que desplegar, asegurar, versionar ni sincronizar. Coherente con el objetivo de despliegue sencillo del proyecto.
- Aislamiento multi-tenant trivial: la búsqueda corre en tenant_{slug} como cualquier otra consulta (ADR-002). Con Elasticsearch habría que replicar el aislamiento (índice/alias por tenant + control de acceso) fuera del modelo ya probado.
- Consistencia fuerte: el search_vector es una columna generada; no hay reindexado asíncrono ni ventana de inconsistencia BD↔índice.
- Una sola fuente de datos y un solo lenguaje (SQL): filtros, texto y agregaciones en la misma consulta; sin ETL ni connectors.
- Suficiente para la escala objetivo (instituciones públicas/privadas latinoamericanas): GIN + trigram rinden bien hasta millones de filas por tenant.
Negativas:
- PostgreSQL FTS es menos potente que Elasticsearch en relevancia avanzada, sinónimos, fuzziness configurable, facetado a gran escala y agregaciones analíticas masivas.
- El diccionario spanish cubre stemming básico; necesidades lingüísticas avanzadas (sinónimos, thesaurus) requieren configurar diccionarios propios.
- A escalas muy grandes (decenas de millones de docs con consultas analíticas intensivas) podría requerirse un motor dedicado.
Mitigación / puerta de salida: si una institución supera lo que PostgreSQL FTS rinde, se podrá añadir un motor externo alimentado por eventos (orpycamcp.*.events, ADR-010) como índice derivado opcional por despliegue, sin cambiar el modelo de datos ni el contrato de la API de búsqueda. La decisión es reversible por diseño.
Alternativas consideradas¶
- Elasticsearch/OpenSearch dedicado: descartado en F2 por el coste operativo (cluster, seguridad, sincronización), la fricción con el aislamiento por schema (ADR-002) y el riesgo de divergencia BD↔índice; su potencia extra no es necesaria a la escala objetivo.
LIKE/ILIKEsimple sin índices: descartado por no escalar (escaneos secuenciales) y no ofrecer ranking.- Extensiones especializadas (p. ej.
pgvectorpara semántica): fuera de alcance de E09; la búsqueda semántica/RAG pertenece a la capa de conocimiento (E21, ADR-006), no a la búsqueda documental de F2.