Saltar a contenido

Search Infrastructure en Camunda 8

Resumen: Camunda 8 implementa una capa de búsqueda backend-agnostic que abstrae Elasticsearch, OpenSearch y RDBMS detrás de una misma interfaz. El patrón arquitectónico es Controllers → Service → SearchQueryRequestMapper → TypedSearchQuery → SearchClientReader → ES/OS → ResponseMapper. La infraestructura se organiza en 9 sub-módulos Maven con ~40 readers en el módulo client-reader, soportando todos los tipos de entidades del sistema.


Motivación: abstracción backend-agnostic

Problema

Camunda 8 necesita consultar datos de runtime (process instances, jobs, incidents, variables, user tasks, etc.) para sus webapps (Operate, Tasklist) y su REST API. Originalmente, esto se hacía directamente contra Elasticsearch, pero:

  1. Lock-in: depender directamente de Elasticsearch acopla la arquitectura a un vendor específico.
  2. OpenSearch: AWS promueve OpenSearch como fork; muchos clientes lo usan.
  3. RDBMS: para instalaciones simples o on-premise, una base de datos relacional puede ser suficiente sin la complejidad de un cluster ES/OS.
  4. Testing: ejecutar tests contra ES/OS real es lento; una abstracción permite mocks o in-memory backends.

Solución

Una capa de abstracción que define interfaces agnósticas al backend, con implementaciones pluggables para cada motor de búsqueda.


Patrón arquitectónico

El flujo completo de una query de búsqueda sigue este pipeline:

flowchart TD
    Ctrl[REST Controller] --> Svc[Service Layer]
    Svc --> Mapper[SearchQueryRequestMapper]
    Mapper -->|"Convierte request<br/>→ TypedSearchQuery"| TSQ[TypedSearchQuery]
    TSQ -->|"Representación interna,<br/>backend-agnostic"| Reader[SearchClientReader]
    Reader -->|"Traduce a query del backend"| Backend[(Backend:<br/>ES / OpenSearch / RDBMS)]
    Backend --> Resp[ResponseMapper]
    Resp -->|"Convierte response<br/>→ modelo de dominio"| DTO[DTO de respuesta]

Componente por componente

Controllers

Los controllers REST exponen los endpoints de búsqueda:

GET  /v2/process-instances?filter.state=ACTIVE&sort=startDate&size=20
POST /v2/process-instances/search (body con filtros complejos)
  • Reciben parámetros de búsqueda (filtros, sort, pagination).
  • Validan el input.
  • Delegan al service layer.
  • No conocen el backend de búsqueda.

Service Layer

Orquesta la lógica de negocio de la búsqueda:

SearchQueryRequestMapper

Convierte el request del controller al modelo interno de búsqueda:

SearchRequest (REST) → TypedSearchQuery<ProcessInstanceFilter, ProcessInstanceSort>
  • Mapea campos del request a campos de filtro.
  • Convierte sort specifications.
  • Aplica defaults (tamaño de página, sort default).

TypedSearchQuery

La representación interna y backend-agnostic de una query:

TypedSearchQuery<F extends FilterBase, S extends SortOption> {
    filter: F,           // criterios de filtrado
    sort: List<S>,       // criterios de ordenamiento
    page: SearchAfter,   // paginación (search_after pattern)
    size: int            // tamaño de página
}
  • Genérica: parametrizada por tipo de filtro y tipo de sort.
  • Inmutable: una vez creada, no se modifica.
  • Sin referencia a backend: no contiene DSL de ES ni SQL.

SearchClientReader

El corazón de la abstracción. Traduce TypedSearchQuery al lenguaje nativo del backend:

// Para Elasticsearch
TypedSearchQuery → SearchRequest (ES Java client) → ES → SearchResponse

// Para OpenSearch
TypedSearchQuery → SearchRequest (OS Java client) → OS → SearchResponse

// Para RDBMS
TypedSearchQuery → SQL query → JDBC → ResultSet
  • Cada backend tiene su implementación del SearchClientReader.
  • ~40 readers distintos cubren todos los tipos de entidades.

ResponseMapper

Convierte la respuesta del backend al modelo de dominio:

// Elasticsearch
SearchHit → ProcessInstanceEntity

// RDBMS
ResultSet → ProcessInstanceEntity

Los 9 sub-módulos Maven

La infraestructura de búsqueda se organiza en 9 módulos Maven, cada uno con una responsabilidad clara:

1. search-domain

  • Define los modelos de dominio de búsqueda.
  • Interfaces de filtros, sorts, y queries.
  • No tiene dependencias de backend.

2. search-client

  • Define las interfaces de los clients de búsqueda.
  • SearchClientReader interface.
  • SearchClientWriter interface (para indexación).

3. search-client-elasticsearch

  • Implementación del SearchClientReader para Elasticsearch.
  • Usa el Java client oficial de Elasticsearch (co.elastic.clients:elasticsearch-java).
  • Traduce TypedSearchQuery a SearchRequest de ES.
  • Maneja connection pooling, retry, y serialización.

4. search-client-opensearch

  • Implementación del SearchClientReader para OpenSearch.
  • Usa el Java client oficial de OpenSearch (org.opensearch.client:opensearch-java).
  • API muy similar a ES client (OpenSearch es fork), pero con diferencias en features avanzados.

5. search-client-reader (~40 readers)

  • Contiene las implementaciones concretas de readers para cada tipo de entidad.
  • ~40 readers, uno por cada entidad consultable del sistema.

Readers por categoría:

Categoría Readers
Process ProcessDefinitionReader, ProcessInstanceReader, FlowNodeInstanceReader, VariableReader, SequenceFlowReader
Decision DecisionDefinitionReader, DecisionInstanceReader, DecisionRequirementsReader
User Tasks UserTaskReader, FormReader
Jobs JobReader
Incidents IncidentReader
Messages MessageReader, MessageSubscriptionReader
Operations BatchOperationReader, OperationReader
Identity UserReader, GroupReader, RoleReader, TenantReader, AuthorizationReader, MappingRuleReader
Infrastructure DeploymentReader, ResourceReader

Cada reader implementa: - Mapping de campos de filtro a campos del índice/tabla. - Mapping de sort options a campos ordenables. - Conversión de respuestas al modelo de dominio. - Paginación eficiente (search_after para ES/OS, OFFSET/LIMIT para RDBMS).

6. search-query-transformer

  • Transformaciones y optimizaciones de queries antes de enviarlas al backend.
  • Rewriting de filtros para eficiencia.
  • Normalización de sorts.
  • Aplicación de filtros de autorización y tenant.

7. search-plugin

  • Sistema de plugins para extender la funcionalidad de búsqueda.
  • Permite agregar filtros custom, transformaciones, o post-procesamiento.

8. search-connect

  • Configuración y conexión a los backends.
  • Connection factories para ES, OS, RDBMS.
  • Health checks del backend.
  • Configuración de indices/aliases/templates.

9. search-rdbms (implícito)

  • Implementación para bases de datos relacionales.
  • Usa Spring Data JPA / JDBC.
  • Soporta PostgreSQL, H2 (para testing).

Readers especializados (~40)

Existen 40+ reader classes, cada uno especializado en un tipo de entidad:

  • ProcessInstanceReader — instancias con state, incidents, tree path.
  • FlowNodeInstanceReader — elementos BPMN ejecutados.
  • VariableReader — variables con scoping.
  • IncidentReader — incidents activos y resueltos.
  • JobReader — estado de jobs.
  • DecisionInstanceReader — evaluaciones DMN.
  • UserTaskReader — user tasks con assignment.
  • ProcessDefinitionReader — definiciones deployadas.

Cada reader encapsula la lógica de mapping específica de su entidad, incluyendo joins denormalizados (como el operate-list-view que usa join relations en ES).


Paginación: search_after pattern

La búsqueda usa el patrón search_after en lugar de offset-based pagination:

Problema con offset

SELECT * FROM process_instances ORDER BY start_date LIMIT 20 OFFSET 10000
-- Problema: el backend debe leer y descartar 10000 rows

Solución: search_after

// Request 1: primera página
{sort: [{field: "startDate", order: "DESC"}], size: 20}
→ Resultados: [item_1, item_2, ..., item_20]
→ searchAfter: ["2026-05-14T10:00:00", "12345"]

// Request 2: siguiente página
{sort: [{field: "startDate", order: "DESC"}], size: 20, searchAfter: ["2026-05-14T10:00:00", "12345"]}
→ Resultados: [item_21, item_22, ..., item_40]

Ventajas: - Rendimiento constante: no importa qué tan profundo se pagine. - Consistencia: si se insertan datos entre requests, no se pierden ni duplican items. - Compatible con ES/OS: es el mecanismo nativo de paginación eficiente.


Backend RDBMS (8.8+)

Desde la versión 8.8, Camunda soporta PostgreSQL como backend de búsqueda alternativo a Elasticsearch. Esto simplifica significativamente el deployment:

  • Elimina la dependencia de ES/OS para instalaciones pequeñas.
  • Usa el mismo PostgreSQL que Identity y el RDBMS exporter.
  • Queries SQL estándar con índices apropiados.
  • Trade-off: sin full-text search avanzado ni aggregations complejas.

Índices de Elasticsearch

Los índices principales que alimentan las webapps:

Índice Contenido Consumidor
operate-list-view Process instances + flow nodes + variables (join relation) Operate
operate-incident Incidents activos y resueltos Operate
tasklist-task User tasks con assignment y forms Tasklist
zeebe-record-* Raw events exportados por ElasticsearchExporter Optimize
operate-operation Batch operations Operate
operate-decision-instance Evaluaciones DMN Operate

El índice operate-list-view es particularmente complejo: usa join relations de ES donde processInstance es el parent y activity + variable son children. Esta denormalización permite queries eficientes pero complica la migración entre versiones.


Consideraciones para un MVP

  • Esencial: una abstracción de búsqueda, aunque sea mínima, evita acoplarse a un backend específico. Incluso si el MVP solo soporta un backend, definir interfaces ahora facilita agregar otros después.
  • MVP mínimo: implementar solo el backend de Elasticsearch (o PostgreSQL para simplicidad) con 5-10 readers para las entidades esenciales (ProcessInstance, Job, Incident, Variable, ProcessDefinition).
  • Simplificable: los 9 sub-módulos pueden consolidarse en 2-3 para un MVP: domain, client-impl, readers.
  • Simplificable: search_after pagination puede diferirse a favor de offset-based para un MVP (aceptando la limitación de rendimiento en paginación profunda).
  • Recomendado: el patrón TypedSearchQuery como representación intermedia es una buena inversión desde el inicio — es lo que permite agregar backends sin reescribir la lógica de negocio.
  • Recomendado: cada reader como clase independiente (en lugar de un mega-reader) facilita testing y mantenimiento.
  • Alternativa simplificada: para una plataforma simplificada, la search infrastructure entera se puede reemplazar con queries SQL directas contra PostgreSQL. Los 9 módulos y 40+ readers se reducen a un repositorio SQL por entidad.

Ver analysis/trade-offs-arquitectonicos para el análisis de la decisión ES vs PostgreSQL.